PHP 8.4 included a new feature for class definitions called “property hooks“. These are a way to dynamically control the behavior of property reading and writing an object property. To put it another way, they let you turn property access into method calls. This is something that many other programming languages do, and it’s nice to see it added into PHP… except that I don’t think they should be used. (How’s that cliffhanger?)
Why methods are great
First a step back in time. Before we had object-oriented programming, we had structs. These are lightweight objects that are just a collection of properties. They are record objects and are generally pretty easy to read and reason about. They allow collecting multiple pieces of data into one piece of data.
class Product {
public string $color; // This can be accessed all over the place without worry.
}
Then, we got object methods (functions attached to an object instance).
Methods are incredibly powerful and allow objects to interact with their own internal data under the hood, allowing for complex operations without needing to understand the underlying mechanism. That is, they provided the ability to make a “black box” system where the internals of an operation are not observable or knowable; only the results. This can be really great from the perspective of maintainability because it defines a clear contract with code that calls the method (this contract is called an API): the caller must provide required data (the method’s arguments, if any) and the method will perform an action, possibly returning a result.
The following method returns a product’s color. We probably don’t need to know what goes on in there!
class Product {
public function get_color(): string {
// ... This is a method so clearly some calculation is happening.
// ... We probably will try to call it less frequently than a property.
}
}
Prior to object methods, if you wanted to derive a complex value from the data in an object, it would need to be done outside the class. And that’s quite a fine approach but it’s not always the right solution because it means that the implementation of that data is tightly coupled to the function that derives the value, and it additionally means that the implementation details must be public. So refactoring may be quite hard if a lot of other things have coupled to that implementation.
With object methods, it’s possible to hide the details and keep refactoring within the class, which is much simpler and safer, especially for a language like PHP which does not come with strong type guarantees outside of runtime.
So, TLDR: methods are great!
If we need to get a value derived from some arcane process, let’s use an accessor method to get it. Because it’s an method, callers should be able to assume that there might be some dynamic operation that occurs when running it, and that if the operation is slow, it could be due to the method being expensive. This helps debugging performance issues, amongst other things.
On the other hand, if a caller is accessing an object property, the caller is likely to assume that it’s just data access, like with a record object. No processing is happening, and so we don’t need to worry about caching or any of the other concerns we might have when calling a function.
The problem with dynamic properties
Here’s the problem: if accessing an object property might sometimes be calling a method under the hood, it can make debugging more difficult. How much more difficult? To be honest, usually not a lot, but the real question is: why not avoid any confusion by just using a method instead? What does a dynamic property actually get us over a method call? It’s the difference between $user->name and $user->name() and that difference might save a lot of mental load for developers.
doSomethingWithColor(
$product->color // Is this a property or a method? How should we behave?
);
The problem with magic methods
Even though property hooks are new, dynamic property access in PHP has actually been around for a long time in the form of PHP’s “magic methods“, specifically the __get() and __set() functions.
These are a heavy hammer and in my experience should be avoided at all costs.
The reason is an extension of the danger of dynamic properties but multiplied because they are one method to handle all possible dynamic variables for a class. In many cases this can make it nearly impossible to track what things are true properties and what are not. There’s no way to use type hints for these properties so there’s no way to perform static analysis on them; any runtime type checking has to be implemented manually.
class Product {
// If we want to know what $product->color does we have to read through this whole thing.
// Can we also set the color property? Who knows. We'll have to look for a magic setter.
public function __get( string $value ): string {
if ( $value === 'color' ) {
return $this->get_color();
}
// ... tons of other dynamic properties here
throw new \Exception("Unknown property {$value}");
}
// ...
}
Property hooks are a much better alternative to magic methods because they are limited to one property at a time, can include type hints, and have explicit rules about their backing values.
How property hooks can help migrations
But property hooks are still brand new, so it’s very likely that if you’re working in an established codebase, you may have some magic methods around. If possible, the best thing to do is to migrate the properties they access to actual methods on the class instead. This will provide type hints, static analysis, clear debugging, and easy readability. Great!
But sometimes this is not easy to do.
If the codebase is very large and the property names are not easily searchable, or if there might be dynamic property assignment going on, it can be extremely difficult to perform this sort of refactor. Remember that static analysis is nearly impossible with magic methods, so we can’t even rely on our tools to be able to find all the places where the dynamic properties are being accessed.
In these cases, property hooks can be just the tool we need.
class Product {
// This is just one property and it has a defined type!
public string $color {
get => $this->get_color();
// Because there is no setter and this is a virtual property, it's read-only.
}
// ...
}
All of this contributes to clarity, readability, and maintainability. Once a class has a magic method, it can be very difficult to remove it as it affects all property access. And a __get() method might also need a __set() and even __isset(). Changing or removing one may require a lot of work in different parts of the class.
A property hook, on the other hand, is self-contained. If we want to remove it or change it, we can do so in one place.
Give me an example
So let’s say we have a simple color property but we need to change it to a method call. What are our options? The need might be urgent. There’s a feature that needs to get out the door or a bug that needs to be fixed.
If you’d like to change all callers to use get_color() instead of color, we could try to change all the call-sites at once, but if the code base is complex there’s a chance we could miss some (a word like color can be difficult to search for in a codebase with 100,000 files). Also the diff might be massive and hard to review, risking accidental bugs. And worse, we might already have a magic method, which complicates things further.
But if we use a property hook, then we only have to make one change inside the class itself! Add the property hook, and all callers are suddenly calling a method instead, with strict type checking built-in.
--- property.php
+++ property-hook.php
@@ -1,4 +1,6 @@
<?php
class Product {
- public string $color;
+ public string $color {
+ get => $this->get_color();
+ }
}
Hey, that’s pretty great. Property hooks are so useful!
Wait…
…
Didn’t I say that dynamic properties are a bad idea?
Ok, yes. But with a property hook, the urgency to do everything at once disappears.
Now it’s much safer to go through and change each call-site one at a time, getting proper reviews for each. The functional change is already complete. And we can use logging inside the hook itself to find any call-sites that we’ve missed.
We also haven’t introduced any magic methods which tend to have a compounding effect the longer they exist in a class. Other developers might start adding dynamic properties to them, because “why not? it’s already there”. That doesn’t happen with a property hook.
Eventually, if time permits, the property hook can be removed, and we’d be back to simple method calls with no magic at all. Even if we don’t, at least there’s just one clearly readable block of code to reference and it can be covered by static analysis.
So, in conclusion: property hooks are really great. But maybe don’t use them if you don’t need to. At least not permanently.
Photo by Elin Melaas on Unsplash

Leave a comment