We use PHPStan as our main static analysis tool: it type-checks the code without running it. It is complemented — not replaced — by Psalm taint analysis for security taint analysis.
Extensions
- phpstan-symfony — understands the Symfony container, controllers and services.
- phpstan-doctrine — understands Doctrine entities and repositories.
- phpstan-phpunit — ensure proper usage of typed assert function (assertTrue, assertCount, ...), better mock intersection type, and also help reduce false positif for tests files
- phpstan-friendly-formatter — nicer error output + Identifier Summary.
- phpstan-disallowed-calls — bans dangerous calls (see below).
Level. Every new project runs at the highest level (10 today), set in the copied phpstan.neon (parameters.level: 10). The level is declared in the config file only,
not on the command line of make phpstan, so that the phpstan.level rule is compliant.
When you first require the standard bundle it will copy the phpstan.neon default config and the qualimetry.mk which set the default level to 10 (in phpstan.neon, not in the make commands).
Approach A: Move up level by level
You need to find the minimum starting level of the project where phpstan returns no errors, then generate the baseline from that level.
You can then fix errors one by one to progressively move up the levels.
While you may have fewer changes to make to become fully compliant with a given level (e.g. full compat level 8), this approach doesn't guarantee that your future development will start directly on the latest rules of the most advanced level 10.
See the PHPStan docs on rule levels.
Approach B: Stay on the max level and use the baseline
We recommend this approach to ensure that all new development starts directly on phpstan's highest standards.
Generate the baseline with the command make phpstan-generate-baseline or its shortcut make pgb.
You can then work through the old errors in dedicated sessions, rule by rule.
See the PHPStan docs on baseline.
Still if this approach generate too much work (to directly apply level 10 on a legacy project) you can still lower back the level and regenerate the baseline from there.
For a specific case in a file, use the comment // qa: False positive %additional explanation% @phpstan-ignore %code identifier%
Otherwise, use the config format to specify a path, for example
ignoreErrors:
# method.nonObject ignored in tests/: fixtures initialize partial objects,
# so some methods might return null before being fully defined.
-
identifier: method.nonObject
path: tests/See the PHPStan docs on ignoreErrors.
Security via disallowedFunctionCalls.
- PHPStan covers the presence-based security checks that taint analysis structurally cannot (a function is dangerous regardless of data-flow).
- We ban, among others:
eval,exec,system,passthru,shell_exec,popen,phpinfo,assert,md5,sha1,error_reporting,ini_set. - Taint analysis such as injection/XSS (data-flow) is handled by Psalm;
- dependency CVEs check by
composer audit.
Why we do not use ekino/phpstan-banned-code.
Note: This PHPStan extension is not used in this project. We rely exclusively on phpstan-disallowed-calls.
- Also listed in the PHPStan extension library, it can look like an alternative to
phpstan-disallowed-calls— it isn't: it only bans global functions and a handful of language constructs (echo,print,eval,exit, backticks), with no support for banning method calls, static calls, namespaces, constants, superglobals or attributes. - It has no per-rule custom
messageand noallowIn/allowExceptInexemption mechanism (both used above and inphpstan.neon). - Its only feature without a
phpstan-disallowed-callsequivalent isBannedUseTestRule(forbids importing test classes from app code) — too narrow on its own to justify switching. Replacingphpstan-disallowed-callswith it would be a functional regression.
When running phpstan and the amount of past ignoreError has been lower due to type correction (or phpstan update or even new phpstan extension) you will see the following messages :
When pattern isn't matched at all
❯ path/to/File.php
-----------------------------------------------------------
✘ Ignored error pattern X in path path/to/File.php was not matched in reported errors.
🏷️ ignore.unmatched
<unknown file line>
For count reduction
❯ path/to/File.php
-----------------------------------------------------------------------------
✘ Ignored error pattern X in path path/to/File.php is expected to occur 3 times, but occurred only 2 times.
🏷️ ignore.count
> 28| Code ...
That means that the previously detected line (28 in this example) is not flagged anymore.
Instead of regenerate the full baseline, and take the risk to ignore remaining error, the best approach is to :
- open the baseline
- find the "path/to/File.php" with the mention "X" pattern
- For each message :
- Pattern not matched : Completely remove the mention ignore pattern for that file
- For count reduction : lower the count value to the new mention value when running phpstan (on the example above the new value must change from 3 to 2)
After that you can launch make phpstan again and the ignore.unmatched / ignore.count will not be shown anymore.
Note that this behavior of detecting count lowering on the baseline is tied to the option reportUnmatchedIgnoredErrors set to true and this is the expected behavior. It ensure that we keep track of fixed error mention on the baseline witch can be reduced that way.
| Command | Purpose |
|---|---|
make phpstan |
run the static analysis |
make phpstan-generate-baseline or shortcut make pgb |
generate / update the PHPStan baseline |
make qa |
run PHPStan + the rest of the qualimetry suite |