skip to content

Baselines & PHPDoc Types

Generating a PHPStan baseline, ignoring errors by identifier, and the PHPDoc types it reads beyond native ones: list<T>, array shapes, @template generics. Asked because legacy adoption hinges on it.

on this pageshow

explore

questions

5

In PHPStan, how do you generate a baseline, what does each entry record, and how does the baseline shrink as errors get fixed?

level: middleimportance: must knowfreq 50%

answer

  1. a generated ignoreErrors list
  2. --generate-baseline, phpstan-baseline.neon
  3. message, identifier, count, path; no line
  4. reportUnmatchedIgnoredErrors defaults to true
  5. 'expected to occur 2 times, occurred only 1'

basics

~20 s

vendor/bin/phpstan analyse --generate-baseline writes phpstan-baseline.neon, an ignoreErrors list with message, identifier, count and path per file, which you include in phpstan.neon; fixed errors leave entries unmatched, PHPStan reports them, and you regenerate to shrink it.

solid answer

~50 s

Run `vendor/bin/phpstan analyse --generate-baseline` (or `-b`); it writes `phpstan-baseline.neon`, which you add to `includes` in phpstan.neon. The file is simply `parameters: ignoreErrors:` entries, one per message, identifier and file, with a `message` regex like `#^...$#` (or `rawMessage` with Bleeding Edge in 2.2), an `identifier` such as `argument.type`, a `count` and a `path`. There are no line numbers. Shrinking is enforced: `reportUnmatchedIgnoredErrors` is true by default, so when you fix one of two counted errors PHPStan fails with *is expected to occur 2 times, but occurred only 1 time*, and a fully fixed entry reports *was not matched in reported errors*. You then rerun `--generate-baseline` with the same path and commit the smaller file. The count also works the other way: a third identical error in that file is reported, not hidden. New errors belong in fixes or inline ignores, not in a regenerated baseline.

code

bash · 5 lines
bash
# first time, or to shrink after fixes (same path as the included file)
vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon

# review: the diff should only remove or reduce entries
git diff --stat phpstan-baseline.neon

go deeper

for a junior

Recall the command, --generate-baseline, the default file phpstan-baseline.neon, and that it must be included in phpstan.neon.

for a middle

Explain the entry fields (message, identifier, count, path), the lack of line numbers, and how unmatched and over-counted entries are reported.

for a senior

Run the shrink workflow in review: regenerate after fixes, reject diffs that add entries, and keep new accepted errors as inline ignores with reasons.

for a principal

Set policy for baselines across teams: size targets, who may regenerate, and when a huge baseline means changing the level instead.

## Generating the file A PHPStan **baseline** is a generated list of the errors that exist today, so that only new errors fail the build. You create it with the analyse command: ```bash vendor/bin/phpstan analyse --generate-baseline ``` - `--generate-baseline` (short `-b`) writes to `phpstan-baseline.neon` by default; a different path can follow the option. - A path ending in `.php` produces a **PHP-format** baseline, which loads faster than NEON when the file grows to megabytes. - If a baseline is already in use, pass the **same path** again, so the old file is replaced rather than applied twice. You then include it from your main config: ```yaml includes: - phpstan-baseline.neon ``` ## What an entry records The baseline is nothing more than `parameters: ignoreErrors:` entries. In PHPStan 2.2 an entry looks like this: ```yaml parameters: ignoreErrors: - message: "#^Variable \\$total might not be defined\\.$#" identifier: variable.undefined count: 2 path: src/Report/MonthlyTotals.php ``` | Key | Meaning | |---|---| | `message` | an anchored, escaped regex of the exact error text (`rawMessage`, a plain string, when Bleeding Edge's toggle is on) | | `identifier` | the error identifier, such as `argument.type` | | `count` | how many times this error occurs in that file | | `path` | the file, relative to the baseline file's directory | There are **no line numbers**. Editing unrelated lines in the file does not break the baseline, but the baseline also cannot say *where* in the file an error is. ## How the counts are enforced The count makes the baseline strict in both directions: 1. **More occurrences than counted**: if someone adds a third identical error to that file, PHPStan reports *... is expected to occur 2 times, but occurred 3 times.* New debt of the same kind is not silently absorbed. 2. **Fewer occurrences than counted**: with `reportUnmatchedIgnoredErrors` at its default `true`, fixing one of two errors produces *... is expected to occur 2 times, but occurred only 1 time.* 3. **No occurrences**: an entry that matches nothing reports *... was not matched in reported errors.* Cases 2 and 3 are what **shrink** the baseline: the build fails until you regenerate it, so fixed errors cannot linger as dead entries that would later hide a regression. ## The shrinking workflow - Fix errors in a file or a whole category. - Run `vendor/bin/phpstan analyse --generate-baseline` with the same baseline path. - Review the diff: it should contain only **removed** or reduced entries. An added entry means a new error slipped in. - Commit the smaller baseline with the fix. Turning `reportUnmatchedIgnoredErrors` off stops cases 2 and 3 from failing the build, which removes the pressure to shrink. It can also be set per entry with `reportUnmatched`. ## What a baseline makes possible The PHPStan docs list three situations where the baseline is the enabling tool: - **Upgrading PHPStan immediately.** A new major version reports new errors in old code; baselining them lets the new rules guard new code from day one. - **Running a higher rule level before the old code is clean.** Code written from now on is checked at, say, level 5 for argument types, while the old violations wait in the file. - **Introducing stricter rules** such as `phpstan-strict-rules` for new code only. In each case the baseline is a temporary holding area, and the count-based reporting above is what keeps it shrinking rather than silently growing. ## What not to do with it The PHPStan docs are direct about regenerating the baseline to hide **new** errors: the baseline's goal is to disappear, it has no line numbers and no place for a comment explaining why an error is accepted. A new error you genuinely must accept belongs in an inline `@phpstan-ignore` with an identifier and a reason, where it stays visible next to the code. Also keep the size realistic. The docs say a baseline works best for dozens to a few hundred errors; a file with 15,000 entries usually means the configuration or the chosen level needs attention first.

  • A developer adds a new argument.type error to a file that already has two baselined ones; does PHPStan report it?
    Yes. The baseline entry carries `count: 2` for that message in that file, and a third occurrence exceeds it, so PHPStan reports that the ignored pattern is expected to occur 2 times but occurred 3 times.
  • Why does CI fail right after someone fixes a baselined error?
    `reportUnmatchedIgnoredErrors` defaults to true, so an entry whose count is now too high, or that no longer matches anything, is reported. Regenerating the baseline with `--generate-baseline` removes or reduces the entry and turns CI green again.

saying these in an interview costs you the question

  • The baseline records the line number of every error
  • A baselined file can gain new errors of the same kind unnoticed
  • Fixed errors disappear from the baseline automatically
  • Regenerating the baseline is the normal way to accept new errors
  • --generate-baseline modifies phpstan.neon directly
open as a page

In PHPStan, what do the PHPDoc types list<T> and array{...} shapes express that PHP's native array type cannot?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Native array only says the value is an array. list<T> means keys 0, 1, 2 with no gaps and values of type T; array{id: int, name?: string} describes each key and its type, optional keys included.

open as a page

In PHPStan, how should a plain string from config become a class-string: narrowed with class_exists() or forced with an inline @var?

level: middleimportance: should knowfreq 30%

basics

~20 s

Narrow it: after if (class_exists($name)) PHPStan treats $name as class-string, and the check also protects runtime. An inline @var is trusted without proof, so the docs call it a last resort and prefer fixing the type at its source.

open as a page

In PHPStan 2, how do you ignore one error with @phpstan-ignore and an identifier, and how does that differ from ignoreErrors in phpstan.neon?

level: middleimportance: should knowfreq 40%

basics

~20 s

Put // @phpstan-ignore argument.type (reason) on or above the line, naming the error identifier; it silences only that error there. ignoreErrors in phpstan.neon matches by message regex or rawMessage, identifier, path and count across files.

open as a page

How would you use PHPStan's @template to type a collection class so that its elements keep their type through add(), get() and foreach?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Declare @template T above the class, use T in @param and @return tags, declare @implements IteratorAggregate<int, T> for foreach, and type usages as Collection<User>; PHPStan then infers User from get() and foreach and rejects adding other types.

open as a page