You are introducing PHPStan into a 300,000-line legacy PHP codebase; how do you choose the starting level and configure the first runs?
answer
- first make level 0 mean something
- unknown classes: scanDirectories, bootstrapFiles
- framework magic needs an extension
- memory limit, tmpDir, parallel
- one level up; level 6 is the wall
basics
~20 sStart at level 0 over your own code only, fix configuration noise such as unknown classes with scanDirectories, bootstrapFiles and framework extensions, get to zero, gate CI there, then raise one level at a time, expecting level 6 to be the big jump.
solid answer
~50 sI would first make the output trustworthy. Run `vendor/bin/phpstan analyse --level 0` over the application's own directories, never `vendor`. On legacy code most level 0 errors are configuration problems, not bugs: classes loaded by a custom autoloader or `require` chains (fix with `scanDirectories`, `scanFiles` or `bootstrapFiles`), framework magic (install the matching extension), and deliberately broken fixtures (`excludePaths`). Set `--memory-limit` or `parameters.parallel` if the run is killed, and `tmpDir` for the result cache. Once level 0 is zero, gate CI on it immediately so no new unknown-class errors land, then raise one level at a time. Levels 1 to 5 are usually real bugs and cheap wins; level 6 (missing typehints) often explodes into thousands of errors, so that is where the team plans work, possibly with a baseline to hold new code to the higher level. The docs note that a baseline suits hundreds of errors, not 15,000.
code
yaml · 13 linesparameters:
level: 0
paths:
- src
- app
scanDirectories:
- lib/legacy-classes
bootstrapFiles:
- tools/phpstan-bootstrap.php
excludePaths:
analyseAndScan:
- tests/*/fixtures/*
tmpDir: var/phpstango deeper
Recall that you start a legacy codebase at a low level such as 0 and raise it step by step.
Explain which configuration keys fix first-run noise: paths, excludePaths, scanDirectories, bootstrapFiles and framework extensions.
Plan the rollout: trustworthy config, an early CI gate, level-by-level progress, the level 6 cost, memory and cache settings, and a small baseline only where it helps.
Set the organisation's target level and timeline, balancing type-debt work against feature delivery, and decide how upgrades and Bleeding Edge are scheduled.
## Goal of the first week On a 300,000-line codebase that has never been analysed, the first PHPStan run at a high level produces tens of thousands of errors, most of them noise. The goal of the first week is not to fix code; it is to get a **configuration whose errors are real** and a **green gate at some level** in CI. ## Step 1: analyse only your own code at level 0 ```bash vendor/bin/phpstan analyse --level 0 --memory-limit 2G src app ``` - **Level 0** checks unknown classes and functions, unknown methods on `$this`, wrong argument counts and always-undefined variables. On old code these reveal both real bugs and gaps in PHPStan's view of the project. - Analyse **only your code**. PHPStan discovers Composer dependencies' symbols automatically; `vendor` does not belong in `paths`. - Large codebases can exceed PHP's `memory_limit`; `--memory-limit` takes a php.ini-style value such as `2G`. PHPStan also runs in parallel child processes by default, tunable under `parameters.parallel`. ## Step 2: turn configuration noise into zero Most first-run errors on legacy code are about **discovery**, not correctness: | Symptom | Likely cause | Fix | |---|---|---| | unknown classes that clearly exist | custom autoloader or `require`-based loading | `scanDirectories` / `scanFiles`, or register the autoloader via `bootstrapFiles` | | unknown class aliases | `class_alias()` at runtime | define them in a `bootstrapFiles` file, which PHPStan executes | | undefined methods on framework objects | `__call`/`__get` magic | install the framework's PHPStan extension | | parse errors in fixtures | files broken on purpose | `excludePaths` | | errors in bundled third-party code | vendored libraries inside `src` | `excludePaths: analyse:` so symbols stay visible | Two further settings pay off early: - `tmpDir` inside the workspace, so the **result cache** can be kept between CI runs and incremental runs take seconds. - `phpVersion` if the analysing machine runs a different PHP version than production. ## Step 3: gate CI at the level you reached As soon as level 0 reports zero, commit `phpstan.dist.neon` with `level: 0` and `paths`, and make CI fail on any error. From that moment no new unknown class or wrong argument count can be merged, which is already worth the effort. ## Step 4: raise one level at a time The levels are cumulative, so each step adds a small family of checks: 1. **1 to 4**: possibly undefined variables, unknown methods on any expression, PHPDoc validation, return and property types, dead code. These are usually real bugs and cheap to fix. 2. **5**: argument types. Expect a noticeable batch of genuine type errors. 3. **6**: missing typehints. On untyped legacy code this is the wall, often thousands of errors that are pure work rather than bugs. 4. **7 and 8**: union types and nullable access, where many production null-pointer bugs are found. 5. **9 and 10**: `mixed`, which only makes sense once most code is typed. At each step: run the next level, estimate the error count, fix or plan, merge, and bump `level` in the config. ## Where baselines and other tools fit - A **baseline** (a sibling topic) records current errors so a higher level applies to new code immediately. The PHPStan docs say it works best for dozens to hundreds of errors and is not the right tool for 15,000; spend that effort on configuration or stay at a lower level. - **Bleeding Edge** and **phpstan-strict-rules** come later, once the target level is green. - Pin the PHPStan version in `composer.json` and upgrade deliberately, so a release with new checks does not break the gate unexpectedly. The answer interviewers want is the order: trustworthy configuration first, a green gate early, then level-by-level progress with the level 6 cost planned rather than discovered.
- Level 0 reports 4,000 unknown classes that do exist at runtime; what do you check first?How those classes are loaded. If a custom autoloader or require chain loads them, PHPStan cannot see them: add their directories to `scanDirectories` or register the autoloader in a `bootstrapFiles` file. If they come from framework magic, install the framework's PHPStan extension. These are configuration fixes, not code fixes.
- Why not start directly at level 6 with a baseline of everything?On a codebase this size the baseline would hold tens of thousands of entries, mostly missing typehints mixed with real bugs, and nobody reviews it. The PHPStan docs say a baseline suits dozens to hundreds of errors. Getting lower levels to zero first fixes real bugs and keeps any later baseline small.
- The first run is killed by the CI runner; what do you change?Raise the memory PHPStan may use with `--memory-limit` (for example `2G`), and, if the runner is small, reduce `parallel.maximumNumberOfProcesses`. Also make sure `vendor` is not in `paths`, since analysing dependencies costs memory for errors you cannot fix.
saying these in an interview costs you the question
- Start at level max and fix whatever it reports
- Add vendor to paths so PHPStan knows all the classes
- Unknown class errors at level 0 always mean dead code
- A baseline of 20,000 errors is a good starting point
- Wait until the level is fully clean before adding PHPStan to CI
- Bootstrap files are only parsed, never executed