Strict rules and bleeding edge
max is the top of PHPStan's levels, not the end of what it can check. The levels are about types: is this value what the code says it is? Plenty of bugs involve values that are exactly the type you declared and still do the wrong thing, because PHP was being helpful.
if (!$code) { ... } // "0" is falsy, so coupon "0" counts as no coupon in_array('1000', ['1e3']); // true: two numeric strings compare as numbers
Both lines are type-correct, so max is happy with them. Both are bugs waiting for the right input.
phpstan-strict-rules
phpstan/phpstan-strict-rules is the PHPStan team's set of extra rules for exactly this kind of code. Among other things it reports:
| Rule | Instead, write |
|---|---|
Non-booleans in if, while, ! and && |
$code === null, $items !== [] |
in_array(), array_search() and array_keys() without strict mode |
in_array($code, $codes, true) |
empty() |
=== null, === '', === [] or count(): say which empty you mean |
Loose == and != |
=== and !== |
| Variable variables, dynamic calls to static methods | Ordinary code |
composer require --dev phpstan/phpstan-strict-rules
includes: - vendor/phpstan/phpstan-strict-rules/rules.neon
Some of it is opinion. empty($coupon) on an object is fine at runtime, and strict rules will still ask you to write $coupon === null. That's a fair trade: empty() means six different things depending on the type, and the reader shouldn't have to work out which. If one rule really doesn't suit a codebase, switch that rule off rather than dropping the whole set:
parameters: strictRules: disallowedEmpty: false
Bleeding edge
Each PHPStan release adds checks that are too disruptive to switch on in a minor version, so they wait for the next major. Bleeding edge turns them on now:
includes: - vendor/phpstan/phpstan/conf/bleedingEdge.neon
You get the next major version's checks early, and upgrading becomes a non-event. The cost is that a minor PHPStan update can bring new errors. Use it on projects already clean at max, and expect a few new errors when you update PHPStan.
| Level max | "PHPStan has nothing left to say" |
|---|---|
| Level max + strict rules | "Why does coupon 0 take nothing off?" |
| Level max + strict rules + bleeding edge | "I have seen the future and it has a line number" |
Power-up
<?php final class Newsletter { public function subscribe(?string $email, array $blocked): void { if (!$email || in_array($email, $blocked)) { return; } if (empty($this->subscribers->find($email))) { $this->subscribers->add($email); } } }
<?php final class Newsletter { /** @param list<string> $blocked */ public function subscribe(?string $email, array $blocked): void { if ($email === null || $email === '' || in_array($email, $blocked, true)) { return; } if ($this->subscribers->find($email) === null) { $this->subscribers->add($email); } } }