Static Analysis in the Quality Pipeline

Understand where static analysis fits alongside automated tests in a PHP quality pipeline, and know when to reach for the dedicated Static Analysis lesson for full PHPStan/Psalm depth.

Automated tests answer "does this code do what I expect when I run it?" Static analysis answers a different, complementary question: "could this code even be correct, based on what the types and structure say, without running it at all?" In a mature PHP project the two run side by side — tests in phpunit.xml, static analysis in a phpstan.neon or psalm.xml — and both gate CI. This lesson is a short refresher on how static analysis fits into the overall testing and quality strategy; for the full lesson on PHPStan levels, Psalm, baselines, and CI wiring, see the dedicated "Static Analysis" lesson elsewhere in this module.

What static analysis catches that tests don't

A test only exercises the specific inputs you wrote it for. Static analysis tools like PHPStan and Psalm read your source code without executing it and reason about every possible path: they catch calling a method that doesn't exist on a class, passing a string where an int is declared, forgetting to check a nullable value before dereferencing it, and dead code that can never run. These are exactly the class of bug that a passing test suite can miss entirely, because nobody wrote a test that walked that particular code path.

Because static analysis is cheap to run (seconds, no database, no fixtures) and deterministic (same input, same output, every time), it's usually run before the test suite in CI — fail fast on a type error before spending minutes running integration tests that were doomed anyway.

• Tests verify runtime behavior for the cases you thought to write. • Static analysis verifies structural/type correctness for every path, without running anything. • Run static analysis first in CI — it's faster and catches a whole class of bugs before tests even start.

Strictness, baselines, and suppressions — the short version

PHPStan and Psalm both support a strictness scale (PHPStan's numbered levels 0-9, Psalm's error levels 1-8) and a baseline file that freezes existing violations so you can adopt analysis on a legacy codebase without fixing everything on day one. New code is held to the target strictness; the baseline shrinks over time as old violations get fixed. Suppressions (@phpstan-ignore-line, @psalm-suppress) should be rare and always carry a comment explaining why — an unexplained suppression is a bug waiting to resurface.

For the complete treatment — how to choose a starting level, generate and shrink a baseline, wire strictness increases into CI, and use suppressions responsibly — see the full "Static Analysis" lesson in this course, which covers PHPStan and Psalm configuration, rules, and CI integration in depth.

• Baseline = a snapshot of existing errors, frozen so CI doesn't fail on legacy code. • Strictness should ratchet upward over time, never downward. • Every suppression should have a comment saying why it's safe. • See the "Static Analysis" lesson for full PHPStan/Psalm level-by-level and CI detail.

Code Examples

The signature declares ?float $amount, meaning null is a valid input, but the body never checks for it. A test that only ever passes a real float would pass and never reveal this. PHPStan reads the declared type and flags the mismatch immediately, without any test being run.

public function format(?float $amount): string Common mistake: Relying only on tests to catch type mismatches means the bug ships until someone happens to write a test that passes null — static analysis catches it on every run, for every path, for free.

Treating a green test suite as proof the codebase has no bugs, and skipping static analysis to save CI time.

Run static analysis and tests together in CI, static analysis first — they catch different, non-overlapping classes of bugs.

Tests only check the paths someone wrote assertions for; static analysis checks every path against the declared types. A project with 100% test pass rate can still have live null-dereference and wrong-type bugs that only static analysis surfaces.

Why is static analysis typically run before the test suite in CI rather than after? A. Because static analysis fixes bugs automatically so tests pass more often B. Because it's fast and deterministic, so failing fast on a type error avoids wasting time on tests that were doomed anyway C. Because PHPUnit requires PHPStan to be installed first D. Because static analysis replaces the need for a test suite entirely

B. Because it's fast and deterministic, so failing fast on a type error avoids wasting time on tests that were doomed anyway — Static analysis runs in seconds with no runtime dependencies, so putting it first in the pipeline gives the fastest possible failure signal before spending time on slower integration tests.

<?php
declare(strict_types=1);

final class OrderTotal
{
    public function format(?float $amount): string
    {
        // Bug: $amount can be null (per the signature), but we call
        // number_format() on it unconditionally. PHPStan level 5+
        // flags this as "Parameter #1 $num of function number_format
        // expects float, float|null given."
        return number_format($amount, 2);
    }
}