This week the new version of PHP: 8.4 was released, with some new and shiny features. Well, technically it was 8.4.1, as 8.4.0 did not include some security patches so it never saw the light of day (strong PHP 6 vibes here 😀).
You can see the full list of changes in the migration guide, and I’ll highlight the one that brings me most joy.
Property Hooks
Do you ever grow tired of typing all the “getter” and “setter” methods for your class properties? Personally, I find it to be a meditative experience, and revisit the class architecture in my mind while my hands are busy with the keyboard. Call me old fashioned, but I never got used to generating them via IDE tools.
Well, with property hooks that mundane job just got easier, the code – cleaner, and developers – a little bit happier. Now you can define getters and setters right in the property definition, and they’ll be called every time the property is accessed. Works pretty much like magic methods assigned to a specific property.
class Sample {
public string $foo = 'bar' {
get => $this->foo . ' (bar-bar)';
set => $value . ' (bar)';
}
}
Every time you set the property value (e.g. $sample->foo = 'bar';), you get “(bar)” added to it. Every time you fetch the property value, you see an extra “(bar-bar)” in the end.
Neat, right? Wait, there’s more!
Virtual Properties and Accidental Fatals
Property hooks also introduce a sub-feature called “virtual properties”. It means that the property you declare doesn’t actually exist, and you cannot write into it. There’s one seemingly simple limitation – a virtual property cannot hold value, so no default value, no assignments, nothing. They also take up no memory, which is definitely a good thing.
The implementation is a bit tricky though, and may lead to errors down the line. You cannot explicitly define a property “virtual”. The property is considered virtual if neither get nor set helpers actually address it.
On one hand, it’s entirely logical, since we don’t really store any value in there. On the other hand, it makes our lives a bit harder because we need to make sure nobody accidentally turns a “backed” (regular) property into a “virtual”. Because if they do, they’ll trigger fatal errors if they miss out on refactoring.
For example, here we introduce a property and define a getter for it. We don’t need a setter, so we just omit it:
class Sample {
public string $foo = 'bar' {
get => $this->foo . ' (bar-bar)';
}
}
Somewhere in the most unexpected part of code, a developer you never met assigns this property a value: $sample->foo = 'bar2';, and then proceeds to leave the company the next day.
Couple years later we decide to refactor that code, and turn the property virtual without even realizing that:
class Sample {
public string $foo {
get => 'virtual bar-bar';
}
}
However, that random property value assignment is still there, and will go unnoticed until somebody runs that piece of code, triggering a fatal at the worst possible moment (as it usually happens):
Fatal error: Uncaught Error: Property Sample::$foo is read-only
Pretty much the same thing happens with properties accidentally turned into write-only virtual ones. If the property only has the “set” method defined, and it doesn’t write into anything the property, it will trigger a similar error:
class Sample {
public string $foo {
set => {
echo $value . ' (virtual write-only set)';
// Here we used to have "$this->foo = $value",
// but we removed it without realizing we turned the property "virtual".
}
}
}
// Here we have some old code that trigger a fatal error as the property is virtual now.
echo $sample->foo;
// Fatal error: Uncaught Error: Property Sample::$foo is write-only
This is an unlikely scenario, but it’s still worth keeping in mind. However, it wouldn’t be a problem if the property had to be explicitly declared “virtual” (e.g. public virtual string $foo) to prevent it from being done inadvertently.

