code quality and tooling

Static Analysis

Static analysis checks PHP code without running the application. It reads files, type declarations, PHPDoc, and configuration to find likely bugs before they reach production.

The two common PHP tools are PHPStan and Psalm. Later lessons cover them directly. This lesson explains the idea behind both.

What static analysis catches

Static analysis is good at finding mismatches that can be understood from the code.

PHP example
<?php

declare(strict_types=1);

function productPriceLabel(array $product): string
{
    return $product['price'];
}

This function promises to return a string, but it returns whatever is stored in $product['price']. If the product price is an integer, a static analyser can report the mismatch.

Types make analysis stronger

The more clearly the code describes values, the more useful static analysis becomes.

PHP example
<?php

declare(strict_types=1);

function formatMoney(int $pennies): string
{
    return 'GBP ' . number_format($pennies / 100, 2);
}

echo formatMoney(499);

// Prints:
// GBP 4.99

The function signature gives the tool a clear contract: pass an integer in, get a string out.

PHPDoc fills gaps

Native PHP can say array, but it cannot describe every key in an array shape. PHPDoc makes static analysis much more precise.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array{name: string, price: int} $product
 */
function reportLine(array $product): string
{
    return $product['name'] . ': GBP ' . number_format($product['price'] / 100, 2);
}

With the docblock, a static analyser can warn when code passes an array without name or price.

Static analysis is not the same as tests

Static analysis checks what can be inferred from code. Tests run code and check behaviour.

Static analysis can catch:

  • passing the wrong type
  • returning the wrong type
  • reading a missing array key when the shape is known
  • calling an undefined method
  • using a class that does not exist
  • unreachable branches
  • PHPDoc that conflicts with code

Tests are still needed for:

  • business rules
  • real database behaviour
  • framework integration
  • user journeys
  • calculations with many examples
  • regressions that depend on runtime state

Use both. They catch different classes of problems.

Levels and baselines

Static analysis tools usually let teams choose strictness. A new project can start strict. An old project may start at a lower level or use a baseline file to record existing issues.

A baseline is not a goal. It is a way to start using the tool without fixing every old issue at once. New code should avoid adding new baseline entries.

Do not blindly silence warnings

When a static analyser reports something, treat it like a reviewer asking a question. It may be wrong, but it is often pointing at unclear code.

Bad response:

PHP example
<?php

declare(strict_types=1);

/** @phpstan-ignore-next-line */
echo $product['price'];

Better response: make the data shape clear, validate the input, or change the code so the tool can understand it.

A practical workflow

A professional static-analysis workflow usually looks like this:

  • run the tool locally
  • read the first error carefully
  • fix the code or improve the type information
  • rerun the tool
  • keep the change small enough to review
  • add the command to CI so errors are caught before merge

The command depends on the tool and project, but it often appears in composer.json scripts.

{
  "scripts": {
    "analyse": "phpstan analyse"
  }
}

Then the team can run:

composer analyse

What to remember

Static analysis is an automated review of your code's type and structure. It works best when the code uses clear native types and accurate PHPDoc.

Before moving on, make sure you can explain one bug static analysis can catch before runtime and one bug that still needs a test.

Practice

Task: Fix A Static Analysis Type Mismatch

Read the code and identify what a static analyser could complain about.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array{name: string, price: int} $product
 */
function productPriceLabel(array $product): string
{
    return $product['price'];
}

echo productPriceLabel(['name' => 'Notebook', 'price' => 499]);

Requirements

  • Identify the type mismatch.
  • Fix the function without changing the input data.
  • Keep the output useful for a human reader.
  • Include the expected output as comments.
Show solution

The function promises to return string, but $product['price'] is documented as int. The fix is to format the integer as a string before returning it.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array{name: string, price: int} $product
 */
function productPriceLabel(array $product): string
{
    return 'GBP ' . number_format($product['price'] / 100, 2);
}

echo productPriceLabel(['name' => 'Notebook', 'price' => 499]);

// Prints:
// GBP 4.99

The PHPDoc says price is an integer, and the return type says the function returns a string. The implementation now satisfies both.