Menu

PHPStan 1.11 With Error Identifiers, PHPStan Pro Reboot and Much More

May 13, 2024 · 15 min read

This release has been in the works for a year. But it doesn’t mean there hasn’t been anything else released for a year. After starting the work on PHPStan 1.11 I continued working on 1.10.x series in parallel, bringing 54 releases into the world between April 2023 and 2024. But at some point earlier this year I said “enough!” and couldn’t resist finally bringing the awesome improvements in 1.11 over the finish line and into the world.

Error Identifiers #

When I decided to add error identifiers I knew it was going to be a lot of work. They’re a way of labeling and categorizing errors reported by PHPStan useful for ignoring errors of a certain category among other things.

I had to go through all PHPStan rules and decide how the identifier should look like for all of them. I’ve settled on two-part format category.subtype pretty early on as being easy to read and memorize. Other tools like TypeScript have settled on simple numbers like TS2322. In my opinion that’s as human-friendly as IP addresses. There are reasons why we need DNS to translate that to actual names.

Another easy way out would be to mark the rule’s implementation class name as its identifier. But I’ve never considered these to be user-facing and stable. They’re implementation details. To give them public meaning would really tie our hands when evolving PHPStan. Because some rule classes report very different issues resulting in multiple identifiers, and at the same time, different rules report very similar issues in PHP code resulting in a single identifier.

So a more flexible approach was needed. You can see for yourself how it turned out. There’s a catalogue on PHPStan’s website with all the identifiers. You can group them by identifier (and see all the rules where the same identifier is reported), or group them by the rule class (and see all the identifiers reported by the same class). This page is generated by analysing PHPStan sources with PHPStan to be used on PHPStan’s website. There’s probably an Inception or Yo Dawg joke somewhere in there.

So let’s say we have an error and we want to find out its identifier in order to ignore it. If we’re using the default table output formatter, we can run PHPStan with -v and see the identifiers right in the output:

Table error formatter running with -v showing error identifiers

Or we can run PHPStan Pro by launching PHPStan with --pro CLI option and see the identifier in the UI next to the reported error:

PHPStan Pro showing error identifiers

Once we know the identifier we can use it in the new @phpstan-ignore comment annotation that replaces the current pair of @phpstan-ignore-line and @phpstan-ignore-next-line annotations which ignore all errors on a certain line. With @phpstan-ignore PHPStan figures out if there’s some code besides the comment on the current line (and ignores an error in that code), or if we want to ignore an error in the code on the next line:

// Both variants work:
// @phpstan-ignore echo.nonString
echo [];

echo []; // @phpstan-ignore echo.nonString

In @phpstan-ignore specifying an identifier is always required.

If we want to ignore multiple errors on the same line, we can place multiple identifiers separated by a comma in the @phpstan-ignore annotation.

echo $foo, $bar;  // @phpstan-ignore variable.undefined, variable.undefined

Yes, we need to repeat the same identifier twice if we want to ignore two errors of the same type on the same line. Better safe than sorry!

We might also want to explain why an error is ignored. The description has to be put in parentheses after the identifier:

// @phpstan-ignore offsetAccess.notFound (exists, set by a reference)
data_set($target[$segment], $segments, $value, $overwrite);

We can also use identifiers in our configuration’s ignoreErrors section. This ignores all errors that match both the message and the identifier:

parameters:
	ignoreErrors:
		-
			message: '#Access to an undefined property Foo::\$[a-zA-Z0-9\\_]#'
			identifier: property.notFound

Or we can ignore all errors of a specific identifier:

parameters:
	ignoreErrors:
		-
			identifier: property.notFound

Across PHPStan core and 1st extensions there’s a total of 728 identifiers in 364 rule classes. [1] I think I managed to strike a good middle ground with the granularity between being too coarse and being too fine.

PHPStan Pro Wizard #

I introduced PHPStan Pro in September 2020 as a paid addon for PHPStan that doesn’t remove anything from the beloved open-source version, but adds extra features that make PHPStan easier to use. Since that PHPStan Pro grew steadily and contributes about one third to my income from PHPStan. That made my open-source work sustainable and I’m really grateful for that.

I’ve had many ideas how to make PHPStan Pro more useful and interesting but I mostly sat on them for the first three years since the initial release, choosing to work on open-source PHPStan instead.

Error identifiers brought the perfect opportunity to turn to PHPStan Pro again. Right now your codebase is probably full of @phpstan-ignore-line and @phpstan-ignore-next-line comments. They ignore all errors on specific lines. All future errors introduced from the point you type out these comments are silently ignored without you ever seeing them. Which is dangerous.

What if you could snap your fingers and all of these comments magically turned into the new @phpstan-ignore with the right identifiers filled out?

- // @phpstan-ignore-next-line
+ // @phpstan-ignore argument.type
  $this->requireInt($string);

PHPStan Pro introduces a migration wizard that’s going to do this for you! Let’s run PHPStan with --pro CLI option and see it in action:

With just a few clicks your codebase can be modernized and made safer thanks to this wizard.

PHPStan Pro now ships with this single wizard, but I have a lot more ideas how wizards could be used to update various aspects of your codebases related to static analysis like typehints and PHPDocs and leave them in a better state. My plan is to do regular “wizard drops” (like feature drops). As the foundation has been layed out, it shouldn’t take long for more awesome wizards to appear!

PHPStan Pro UI Revamp #

But wizards are not the only thing that has changed about PHPStan Pro. Since the beginning PHPStan Pro has been about browsing errors in a web UI instead of a terminal.

You’re probably familiar with this saying:

If you’re not embarrassed by the first version of your product, you’ve launched too late.

Well I’m definitely not guilty of that 🤣 PHPStan Pro up until today did not have a good UI. Each error was rendered in a separate box so if you had multiple errors around neighbouring lines, you could quickly lose context. They weren’t presented in a clear and understandable way. Additionally, you could lose the focus on currently showing file if you reloaded the webpage. The sidebar panel wasn’t sticky so it shifted away if you scrolled a long file. It was not possible to remap Docker path to the host’s filesystem.

I could continue down this unending embarassing list of shortcomings of the previous version, but let me put an end to that.

The new version you’ll get once you update to PHPStan 1.11 and launch Pro with --pro is much better. Let’s have a look:

The layout is more natural and resembles an IDE. Each file is rendered just once and errors are attached to lines where they are reported. You can expand hidden lines to see more context around an error. Docker paths can be remapped so that editor links lead to the correct file.

PHPStan lets you see ignored errors too. If you’ve inherited a project and want to see what errors the previous team ignored, or if you want to check the errors lurking in your huge baseline, PHPStan Pro lets you do that with ease. By default it will show a small pill button you can use to see ignored errors near normally reported errors, but you can also change the setting to show and browse all ignored errors:

PHPStan Pro analyzes your codebase in the background and refreshes the UI with the latest errors automatically. If you don’t want to keep your hands warm while your laptop struggles to keep up with too-frequent analysis, you can choose to run it only when the PHPStan Pro window is in focus, or pause it altogether:

PHPStan Pro costs €7/month for individuals and €70/month for teams. If you decide to pay annually, you’ll get PHPStan Pro for 12 months for the price of 10 months – that’s €70/year for individual developers, €700/year for teams.

There’s a 30-day free trial period for all the plans. And there’s no limit on the number of projects - you can run PHPStan Pro on any code you have on your computer.

Start by running PHPStan with --pro or by going to account.phpstan.com and creating an account.

Is this function really pure? #

We’re not even close to the end of the list of what’s new in PHPStan 1.11.

The @phpstan-pure annotation has been supported for a long time to mark functions that don’t have any side effects. That’s useful for remembering and forgetting returned values among other things.

But PHPStan didn’t enforce the truthiness of this annotation. It always obeyed it. This release changes that. I went through the trouble of deciding what language construct is pure or not for all statement and expression types. PHPStan now understands for example that $a + $b is always pure but calling sleep(5) or assigning a property outside of constructor is impure.

Once we have that information we can use it for multiple opportunities as marking some pieces code as wrong. Pure expressions always have to be used so it doesn’t make sense to have these on a separate line without using the results:

$a + $b;
new ClassWithNoConstructor();
$cb = static function () {
    return 1 + 1;
};
$cb(); // does not do anything

Besides reporting functions annotated with @phpstan-pure but having side effects as wrong, we can now also afford marking functions with @phpstan-impure without any side effects also as wrong. And finally, void-returning functions (which are understood as impure implicitly) that do not have any side effects are also reported as wrong. What would be a point of calling a function that doesn’t have a side efffect and doesn’t return any value?

Clamping this problem space from all sides allowed us to weed out some bugs by testing development versions on real-world projects.

For all of this to be applicable to real-world situations, we also added new pure-callable and pure-Closure PHPDoc types that can be called even in pure functions.

Make sure to enable bleeding edge to take advantage of these new rules.

When is a passed callable called? #

Callables are severely underdocumented in PHP projects. You could already type and enforce the signature of a callable to make it more useful for static analysis:

/**
 * @param callable(int, int): string $cb
 */

But other aspects of callables remained mysterious for PHPStan. What happens to a callback when it’s passed to another function? Is it called immediately or later? This information can become handy for purity checks described in previous section, and also for bringing exceptions under control.

$this->doFoo(function () {
    if (rand(0, 1)) {
        throw new MyException();
    }
});

It would be useful for PHPStan to know if this call to method doFoo should be surrounded with a try-catch or if the thrown MyException should be documented in @throws PHPDoc tag. But it needs to know if this callback is called immediately, or saved for later invocation.

PHPStan 1.11 introduces a new pair of PHPDoc tags for this purpose:

  • @param-immediately-invoked-callable
  • @param-later-invoked-callable

The default reasonable convention if these tags are not present is that callables passed to functions are invoked immediately, and callables passed to class methods are invoked later.

You need these PHPDoc tags only to override the defaults, so you’ll use @param-immediately-invoked-callable above methods and @param-later-invoked-callable above functions.

What is a passed closure bound to? #

PHP’s methods Closure::bind and Closure::bindTo are used to change what $this refers to in a non-static closure. Doing that can lead to confusing PHPStan:

// we're in class Foo
// $this is Foo here
$this->doSomething(function () {
    // PHPStan still thinks $this is Foo here
    // but it could be something different
});

If your function binds passed closure to some other object, you can now use new PHPDoc tag @param-closure-this to inform PHPStan about it:

/**
 * @param-closure-this \stdClass $cb
 */
function doFoo(callable $cb): void
{
    $cb->bindTo(new \stdClass());
    // ...
}

It fully works with generics and conditional types so you can really go crazy in there!

New options for possibly nonexistent offsets in arrays #

PHPStan by default tries not to complain too much about code that might be totally fine. One example is accessing offsets of arrays without checking that these offsets actually exist first.

/**
 * @param array<string, Item> $items
 */
function doFoo(array $items, string $key): void
{
    // this might exist but might not
    $selectedItem = $items[$key];
}

Tom De Wit kindly contributed a pair of new configuration options to have potential these issues reported.

To have the error reported in the example above, turn on reportPossiblyNonexistentGeneralArrayOffset.

To have the same error reported for constant arrays, also known as array shapes, turn on reportPossiblyNonexistentConstantArrayOffset.

public function doFoo(string $s): void
{
    $a = ['foo' => 1];
    echo $a[$s];
}

My personal preference is to only turn on the latter option, but your experience may vary.


Me and PHPStan contributors put a lot of hard work into this release. I hope that you’ll really enjoy it and take advantage of these new features! We’re going to be waiting for everyone’s feedback on GitHub.


Do you like PHPStan and use it every day? Consider sponsoring further development of PHPStan on GitHub Sponsors and and also subscribe to PHPStan Pro! I’d really appreciate it!


  1. The fact there are exactly two identifiers per rule class on average is a total coincidence. It made me re-check my algorithm that counts them. ↩︎

© 2016–2024 Ondřej Mirtes