PHPStan is ready for PHP 8!
November 24, 2020 · 9 min read
PHP 8 is just around the corner! And it’s massive. PHPStan is ready to analyse your codebases that will be taking advantage of the latest features in the coming weeks and months.
I’ll leave the job of describing all the new features and changes to others that specialize in that, and as usual, I’ll geek out a bit about the challenges linked with making PHPStan understand a new major version of the language, and also describe new implemented checks you’ll be able to enjoy alongside the new language features.
Make sure you have PHPStan 0.12.57 or later installed before you start experimenting with PHP 8! 💪
Match expression #
Ambitious language feature from PHPStan contributor Ilija Tovilo that aims to be a better alternative to a switch statement.
Since the match
expression uses a strict comparison equivalent with the ===
operator, we can afford to report type mismatches - arms that will never be executed.
Arms also might not be executed if one of the arms is a catch-all because the comparison will be always true.
The match
expression also throws an exception when the given value isn’t handled by one of the arms. Since PHPStan knows enough information about the code, it can detect it as well. I hope that this feature will lead to using more advanced types in PHPDocs, which in turn will actually lead to more type safety (PHPStan will point out wrong values going into the function at the callsite):
/**
* @param 1|2|3 $i
*/
function foo(int $i): void {
match ($i) {
1 => 'foo',
2 => 'bar',
3 => 'baz',
};
}
Named arguments #
I’m a big fan of named arguments. They will make function calls with multiple arguments more clear [1].
return in_array(needle: $i, haystack: $intList, strict: true);
Implementing support for this feature was pretty straightforward, so I took extra care with user-facing error messages. This is how an ordinary error looks like when named arguments aren’t involved:
Parameter #2 $b of function foo expects int, string given.
With named arguments, the order is no longer significant, so when they’re involved in a function call, I’m removing the parameter number: [2]
Parameter $b of function foo expects int, string given.
When the developer passes a wrong number of arguments to a function, the ordinary error looks like this:
Function foo invoked with 1 parameter, 2 required.
When a named argument is used in the function call, it’s much nicer to show this:
Missing parameter $b (int) in call to function foo.
So that’s what PHPStan does.
Changed function signatures #
Some functions changed their signatures, for example curl_*
functions no longer return resource, but a CurlHandle
object. Many functions have removed false
from possible returned values and throw ValueError
instead.
Fortunately, PHP 8 starts to offer official stubs that we can take advantage of here. I created a new repository that allows including those stubs as a Composer dependency. It’s automatically updated each night to mirror the latest changes in php-src.
It wasn’t straightforward to start using those stubs, because they don’t contain all the information PHPStan needs, so for example they cannot be used in place of jetbrains/phpstorm-stubs. Class definitions do not contain constants and properties, some global constants are referenced but not defined etc. So PHPStan only reads parameter types, parameter names, return types, and PHPDocs, and uses them in a very customized way.
Also, I didn’t want to lose other metadata we already have in functionMap.php, like what value types are in typehinted arrays, or callback signatures. So in the end all of this information is merged together in the final definitions used during the analysis.
Constructor property promotion #
I really like this feature, because it decreases the number of times an injected property name needs to mentioned from 4 to 1. A lot of boilerplate will be simplified.
The most interesting part of the implementation was finding out how people would write additional type information with PHPDocs. Sure, we have typed properties since PHP 7.4, but for example in case of array
, we need to know what’s in it, so PHPDocs are still necessary in some cases.
Which style of additional type info in PHPDoc for promoted properties will you prefer in PHP 8?
— Ondřej Mirtes (@OndrejMirtes) November 1, 2020
Poll in the thread 👇 pic.twitter.com/R5DVAiRi1O
Since the Twitter poll ended with 74 %/26 % split, I decided to implement both variants. 26 % is still a lot of people.
PHPStan will also check that you haven’t declared duplicate properties with the same name, and that you haven’t tried to write a promoted property in another method than constructor (which isn’t a parse error).
Nullsafe operator #
Adding support for this one (also by Ilija Tovilo) was more work than I originally expected. PHP-Parser added two new AST nodes to represent this operator: NullsafeMethodCall
and NullsafePropertyFetch
. So I had to go through all the code where the usual MethodCall
and PropertyFetch
nodes are mentioned[3] and make sure it also makes sense for handling the nullsafe variants.
Another tricky part was the short-circuiting. I’ve had to reread this part of the RFC several times before I realized it has two implications for PHPStan:
- When analysing an ordinary method call or a property fetch, the result might be nullable even if the called method and accessed property aren’t nullable, because there might be a nullsafe operator earlier in the chain.
- The
$foo
in$foo?->bar()
will not be nullable when referenced again in the same chain.
PHPStan will also tell you if you’re using ?->
where an ordinary ->
would suffice, on level 4.
The RFC also disallows assign-by-ref when the nullsafe operator is involved, PHPStan will tell you about that too.
Nullsafe operator also cannot be used on the left side of an assignment.
And the nullsafe operator cannot be used with parameters passed by reference and as a return value in function that return by reference.
As I said - a lot of work 😅
$object::class #
PHP 7 allows you to get a string with a class name with Foo::class
. PHP 8 allows you to access ::class
on an object.
Did you know that PHP does not check whether
Foo
exists and will happily create any string like that? Fortunately PHPStan checks that for you 😊
During the implementation I found out that the accessed variable cannot be a string, so PHPStan also checks for that.
Attributes #
After finally deciding on the syntax, attributes have a bright future ahead. I’m especially looking forward to using them as part of Doctrine ORM entities instead of current PHPDoc-based annotations.
Validation of attributes in PHP runtime itself is postponed until the code tries to call newInstance()
on the obtained ReflectionAttribute
instance. The RFC specifically mentions that static analysis is a great fit to validate attribute usage so that the user isn’t surprised when they run the code that reads and instantiate the attributes.
PHPStan provides the following checks related to attributes:
- #[Attribute]-annotated class cannot be abstract and must have a public constructor (playground example)
- Attribute name used in code must be an existing class annotated with #[Attribute] (playground example)
- Attribute class constructor must be called correctly (playground example)
- Attribute class can be used only with the specified target(s) (playground example)
- Non-repeatable attribute class cannot occur multiple times above the same element (playground example)
New Docker image #
If you prefer to run PHPStan through Docker, I recommend you to switch to a new image hosted in GitHub Container Registry: ghcr.io/phpstan/phpstan
It’s based on PHP 8. If you want to analyse a codebase as if it was written for an older PHP version, change phpVersion
in your phpstan.neon
:
parameters:
phpVersion: 70400 # PHP 7.4
See the image’s homepage in GHCR, or the documentation here on phpstan.org.
The old image hosted on DockerHub is now deprecated.
Do you like PHPStan and use it every day? Support the development by checking out and subscribing to PHPStan Pro. Thank you!
A popular “clean code” argument is that a function shouldn’t have more than a few arguments, but it’s not always possible nor practical. ↩︎
Good UX is getting thousand tiny little things right. ↩︎
Arguably the most common thing PHP developers do is calling methods and accessing properties so there’s a lot of concerns in PHPStan handling those. ↩︎