Living with a baseline
Raise a real project from level 6 to level 9 and PHPStan finds four hundred errors. You can't fix them in one pull request, and you can't merge a red build. A baseline gets you out of that corner:
vendor/bin/phpstan analyse --generate-baseline
PHPStan writes every current error to phpstan-baseline.neon, and your phpstan.neon includes it:
includes: - phpstan-baseline.neon
The build goes green at the new level straight away. New code is held to level 9 from now on, and the old errors are written down instead of forgotten. That's the deal a baseline offers: no new errors today, and fewer old ones every week.
What an entry is
- message: '#^Cannot access property \$pence on Zone\|null\.$#' identifier: property.nonObject count: 1 path: src/ShippingQuote.php
Each entry ignores one message, in one file, a set number of times. It has no line number, so the entry survives edits elsewhere in the file. A second error with the same message in the same file goes over the count, and PHPStan reports it.
Read that entry again. It isn't a style complaint. It says that ShippingQuote reads a property from something that can be null, and every country without a zone of its own will crash it. A baseline entry is a bug report with the line number taken off. Some are cosmetic. Some are the reason a customer can't check out.
Make it shrink
PHPStan reports an entry that no longer matches anything:
Ignored error pattern #^Cannot access property \$pence on Zone\|null\.$#
(property.nonObject) in path src/ShippingQuote.php was not matched in reported errors.
That isn't PHPStan nagging. It means you fixed something, so delete the entry and the baseline is one shorter. reportUnmatchedIgnoredErrors is on by default. Leave it on, or the baseline only ever grows.
A good habit: when you open a file for any reason, fix its baseline entries too and leave it with none. Every pull request moves the number down a little.
Ignore one thing, and say why
Sometimes PHPStan is wrong, or a library's types are. You need to ignore one error on one line, not a whole file of them, and not in the baseline, which is for debt you mean to pay off. Use an inline ignore with the identifier:
// @phpstan-ignore argument.type (the SDK's stub says string, but it accepts an Address; VENDOR-311) $this->courier->send($parcel->address);
Only argument.type on that line is ignored. Any other error on it is still reported, and if the SDK fixes its stub, PHPStan reports the ignore as unmatched. The bare @phpstan-ignore-line and @phpstan-ignore-next-line hide everything on the line, including whatever goes wrong there next year.
| phpstan-baseline.neon, January | 12 entries |
|---|---|
| phpstan-baseline.neon, June | 340 entries |
Level 9
This exercise runs at level 9, which is strict about explicit mixed. Once something is declared mixed, the only thing you can do with it is pass it on to something else that accepts mixed. You can't add it, concatenate it, or call methods on it. array<string, mixed> is the usual way mixed gets in: it fixes a level 6 error by saying nothing at all about the values.
Power-up
<?php final class DispatchParcel { public function __invoke(Parcel $parcel): void { // @phpstan-ignore-next-line $this->courier->send($parcel->address, $parcel->weightInGrams()); } }
<?php final class DispatchParcel { public function __invoke(Parcel $parcel): void { // @phpstan-ignore argument.type (the SDK's stub says string, but it accepts an Address; VENDOR-311) $this->courier->send($parcel->address, $parcel->weightInGrams()); } }