PHPStan 2.1: Support For PHP 8.4’s Property Hooks, and More!
December 31, 2024 · 7 min read
This release is a culmination of the last four months of my work. Beginning in September, I branched 2.0.x off of 1.12.x. In order to support PHP 8.4 syntax, I had to upgrade to PHP-Parser v5. In order to upgrade to PHP-Parser v5, I had to release PHPStan 2.0. Which I did on November 11th, alongside the cute and also long-awaited PHPStan elephpant. PHPStan’s fans ordered 750 of those! They will be manufactured in China and sent on a ship to Europe which is going to take some time. I hope they’re going to find themselves in the hands of their happy owners in May-June 2025, as promised in the order confirmation emails.
Today’s PHPStan 2.1 brings full understanding of flagship PHP 8.4 features like property hooks, asymmetric visibility, and the #[Deprecated]
attribute. As usual I’m going to describe what considerations went into supporting these language features in PHPStan.
Property hooks #
This is a massive and complex new language feature which is obvious just by looking at the length of the RFC. After reading it I wrote down about 70-80 todos about what should be done in PHPStan’s codebase in order to properly support it 😅 Out of those there are still around 32 todos left. They are not necessary enough to be included in this initial release, but it’d be nice to have them.
In short, property hooks let you intercept reading and writing properties with your own code. They also allow properties to be declared on interfaces for the first time in PHP. You can have virtual properties without a backing value stored in the object. And last but not least, hook bodies can be written with either long or short syntax.
Property hooks are very similar to class methods, but they are also their own thing. Which means I had to adjust or replicate a lot of existing rules around methods:
- Missing return
- Returned type
- Existence of parameter types
- Compatibility of PHPDocs with parameter types
- Syntax errors in PHPDocs
- Unknown
@phpstan-
tags in PHPDocs - Checked exceptions thrown in the body must either be handled or documented with
@throws
- Rule for unused private properties must ignore property access inside hooks
- Attributes above property hooks
Additionally, there are new special rules for property hooks:
- Backing value of non-virtual property must be read in get hook
- Backing value of non-virtual property must be always assigned in set hook
Set hook parameter type can be wider than the actual property type so PHPStan also has to be aware of the types it allows to assign to properties in and out of hooks:
class Foo
{
public int $i {
get {
return $this->i - 10;
}
set (int|string $value) {
$this->i = $value; // string not allowed
}
}
}
$f = new Foo();
$f->i = rand(0,1) ? '10' : 'foo'; // string allowed
Property hooks also bring properties that can only be read, or can only be written, to the language:
interface Foo
{
public int $i { get; }
public int $j { set; }
}
function (Foo $f): void {
$f->i = 42; // invalid
echo $f->j; // invalid
};
And my final remark is that access to properties can now be expected to throw any exception:
interface Foo
{
public int $i { get; }
}
function (Foo $f): void {
try {
echo $f->i;
} catch (MyCustomException) { // dead catch on PHP 8.3-, but not on 8.4+
}
};
Even non-hooked properties are subject to this, because they can be overwritten in a subclass with a hook. If you want to persuade PHPStan that access to your properties cannot throw exceptions, make them private
or final
.
Hooks can be documented with @throws
PHPDoc tag which improves analysis of try-catch blocks, same as with functions and methods.
I also posted a community update which goes into depth what you should do if you want to support property hooks in your custom rules.
A huge thanks to Jarda Hanslík for initial property hooks support in BetterReflection, and for many many subsequent fixes for bugs I found 😅
And also a huge thanks to Nikita Popov, not just for his work on PHP-Parser in general, but also for merging a couple of fixes I made, and also for fixing a couple of other issues I reported.
Asymmetric visibility #
PHP now allows for different properties visibility when reading then and when assigning them. So property can for example be publicly readable, but only privately writable.
This feature is much easier to grasp than property hooks, but also comes with a few gotchas.
- Property that’s public-readable or protected-readable, but only privately writable, is implicitly final and cannot be overridden.
- Acquiring reference to a property follows write visibility, not read visibility, so I added a new rule for that.
Similarly to property hooks, I also have a few nice-to-have todos left for later.
#[Deprecated]
attribute #
This attribute allows the PHP engine trigger deprecated warnings.
I suspect it’s not that useful in practice because it’s allowed only above functions, methods, and class constants. Most notably, it can’t be used to mark entire classes deprecated. It also does not work for properties. This is something that PHP could address in the future.
Nevertheless, you can use it in PHPStan 2.1 in tandem with phpstan-deprecation-rules to mark deprecated code and have it reported when used.
Do you like PHPStan and use it every day? Consider sponsoring further development of PHPStan on GitHub Sponsors and also subscribe to PHPStan Pro! I’d really appreciate it!