PHPStan level 9 on a legacy codebase: how far did you actually get
Inherited a ten year old PHP app, no types, arrays as data structures everywhere, plenty of magic. Management wants static analysis. I started at level 0, which already found real bugs, and the plan is to climb. But I am skeptical that level 9 is realistic here without rewriting half the code, and a baseline of 8000 ignored errors feels like cheating.
For people who took a genuinely legacy codebase up the levels, where did you stop, and did the high levels catch bugs that the lower ones missed, or just noise about mixed types?
We took a similar codebase to level 6 and stopped there deliberately. Levels 0 through 5 found genuine bugs: undefined variables, wrong argument counts, calling methods on possibly-null, dead code. Levels 7 to 9 are mostly about the mixed type, and on a codebase that passes arrays around as ad hoc structs, that is a flood of true-but-not-actionable warnings until you add real types. We invest in types where they matter instead of chasing the level number.
The baseline is not cheating if you use it correctly. Generate it once so the suite passes, then make the rule that the baseline can only shrink, never grow, enforced in CI. New code is written at the target level, old errors get burned down as you touch files. That is the only way a big legacy codebase ever gets clean, all at once is a fantasy.
The high levels become worth it only after you adopt array shapes or value objects. If you annotate your array-soup with phpstan array shape types or replace the worst offenders with small DTOs, level 8 and 9 suddenly catch real null and missing-key bugs instead of shrugging mixed at you. So the order is: climb to 6, introduce types on the hot paths, then the upper levels pay off. Reversing that order is just pain.
Level 6 as a real target with 8 and 9 as aspirational on typed modules is a sane plan, thanks. The baseline-only-shrinks-in-CI rule is exactly the governance I was missing, that turns it from a vanity metric into a ratchet. We already found a handful of genuine null dereferences at level 4, so the lower levels are clearly earning their keep.
Add the strict rules extension once you are comfortable, it catches things core PHPStan allows, like loose comparisons and switch without default. But do that as a separate ratchet with its own baseline, do not bundle it with your level climb or you cannot tell which change caused which error. One axis of churn at a time keeps reviews sane.
```php blocks are runnable.