PHPStan logo

PHPStan gained a milestone release at the start of November 2021 with new features, extra rules, and many performance optimizations. It means the leading PHP static analyzer is now considered to be stable and comes with a backwards compatibility promise for future updates.

As it’s a major semver bump, PHPStan 1.0 also brings along some breaking changes which could affect your existing scans. These include extra rules and some replaced or removed config parameters.

The New Level 9

PHPStan scans your PHP source to find potential issues without actually running the code. Alongside unit and end-to-end tests, this gives you visibility into the quality of your code by surfacing problems before users run into them.

PHPStan levels are used to configure the analyzer’s strictness. The v0.x release series offered eight levels, with 0 the most relaxed and 8 the strictest. Although it’s preferred to use the strictest level possible for the best coverage, less stringent levels can help you introduce PHPStan to an imperfect codebase.

PHPStan 1.0 adds level 9 as a new option. It includes all the rules from level 8 and lower, as well as one extra check: strict mixed type comparisons.

The mixed type arrived in PHP 8.0 as a way to explicitly typehint “any” value. As mixed is essentially equivalent to an untyped value, it’s unsafe to make any assumptions about what a mixed-type value looks like.

PHPStan’s ninth level will strictly enforce this by reporting an error if you try to actively use a mixed value in your code. Accessing a property or calling a method isn’t allowed as you can’t know whether they’ll exist. You can only pass values through other mixed-type typehints when this rule is active.

To opt-in to the new behavior, you’ll need to change level: 8 to level: 9 in your phpstan.neon config file. PHPStan also supports level: max as an alias for the highest level. If you’re already using max, you’ll automatically get level 9 when you upgrade to PHPStan 1.0.

Better Awareness In Try-Catch-Finally Blocks

PHPStan now has better type inference and variable awareness for try-catch-finally blocks. It’ll use the @throws docblock tag to check the exception types thrown by each function in your codebase. This understanding is used to inform variable availability checks within try-catch blocks.

Here’s an example demonstrating why this matters:

 * @throws DemoException
function first() {
    throw DemoException();
 * @throws OtherException
function second() {
    throw OtherException();
try {
    $first = first();
    $second = second();
catch (DemoException $ex) {
catch (Exception $ex) {
    error_log("General exception");

The first catch blog accesses $second but this won’t exist when DemoException is caught. PHPStan v1.0 uses the @throws statement to realize this so you’ll be informed when your catch blocks reference possibly undefined variables. Functions without @throws annotations will generally behave in the same way as before.

As a result of this change, the optional polluteCatchScopeWithTryAssignments parameter has been removed. This used to let you access variables set in a try block within the following catch; it’s no longer needed as PHPStan can now determine which variables are available.

Unused Code Detection

PHPStan has got better at finding and reporting some forms of unused code in your project. It will highlight private class properties, methods, and constants which are never called or accessed.

Their presence is almost always unintentional. If you do want to keep dead code around for longer, you could try commenting it out or adding the @phpstan-ignore-next-line comment to bypass the check.

Improvements to Remembered Values

v1.0 enhances the consistency of PHPStan’s memory for function return values. It’s more adept at understanding when a function is called a second time, offering better anticipation of identical return values.

This leads to improved dead code detection when a condition is repeated several times. PHPStan will warn when a block becomes redundant because execution is terminated by an earlier branch with the same condition:

if ($demo -> isActive()) {
if ($demo -> isActive()) {

The second block will never run as the first always terminates. There is an assumption at play though: isActive() must always return the same value through the lifetime of $demo. In more concrete terms, isActive() needs to be a pure function where repeated inputs always produce identical output.

PHPStan assumes functions are pure by default. In cases where they’re not, you can add the @phpstan-impure annotation in a docblock above the function’s definition. This will disable the return value memory for that function. You’d need to use this in the above example if isActive() could return a different value to each call, meaning the first check could equate to false and allow the second branch to run.

/** @phpstan-impure */
public function isActive() : bool {
    return (date("Y") === "2021");

Other Rule Improvements

Several new rules have been added to the existing levels 1 to 6. These include checks for overriding constants and properties, trying to extend a final class, and detection of always-true and always-false while loop conditions.

Types are now checked for missing typehints recursively. This means that PHPDoc definitions like array<array> won’t be accepted as they lack an inner type definition. You’ll need to typehint the expected values of the second-level array elements too, such as array<array<string>>.

Besides checking source content, PHPStan 1.0 looks at the overall validity of .php files too. Level 0 gains a check for leading and trailing file whitespace and BOM, as misplaced characters before or after the <?php ... ?> tag pair can cause unexpected output at runtime.

Performance and Stability

Performance has been improved in several areas by optimizing specific operations and resolving some memory leaks. These should contribute to faster and more reliable scans of larger codebases.

As v1.0 is considered a formal stable release, it comes with an assurance that future minor releases (1.1, 1.2, 1.3, etc.) will be backwards compatible. While getting from v0.x to v1.0 may require some config tweaks, you’ll have an easier migration path in the future.

Developers of PHPStan extensions and custom rulesets also benefit from having a more stable codebase that’s less likely to change. The developer documentation has been expanded too, including more accessible coverage of custom rule creation. It should be easier to get started when implementing your team’s own rules into your PHPStan analysis.

While this bodes well for future support, both users and extension developers face breaking changes in the move to v1.0. Several scan configuration parameters have been renamed or changed, with bootstrap becoming bootstrapFiles and excludes_analyse replaced with excludePaths.

Internally the extensions API has been extensively revised so many classes and methods have been altered or removed. The full list of backwards incompatible changes is available in the v1.0 changelog.


PHPStan v1.0 matures the project by optimizing performance, adding new rules, and addressing some detection irregularities. The evolution of PHPStan over the past several years has contributed towards the PHP community’s broader shift to embrace typed language concepts. The addition of features such as typed properties and union types has produced a language that’s capable of supporting advanced external inspection, as well as detailed internal introspection via reflection.

The promotion to a semver stable release should help encourage further PHPStan adoption throughout the community. Migrating to the new version should be fairly pain-free for modern projects that are already using a strict PHPStan rule level.

You can upgrade to PHPStan v1.0 today by running composer require phpstan/phpstan in your projects. It’s advisable to read through the release notes first to identify the breaking changes you need to address. The presence of extra rules in each existing level may cause your codebase to start failing tests even if it passed with PHPStan v0.x. You can disable the new rules temporarily by adding them to your baseline so they’re excluded from scan results.

Profile Photo for James Walker James Walker
James Walker is a contributor to How-To Geek DevOps. He is the founder of Heron Web, a UK-based digital agency providing bespoke software development services to SMEs. He has experience managing complete end-to-end web development workflows, using technologies including Linux, GitLab, Docker, and Kubernetes.
Read Full Bio »