With PHP 8.4, property hooks are introduced which are set to transform the way we handle object properties. Does it truly do this? Following many years of arguments and changes, PHP introduces C#-style property accessors. Still, as is common with new PHP features, the community remains divided.
In this article, I’ll review property hooks, looking at what they can accomplish, if we really need them, what issues they may cause and why you should be careful before changing your entire codebase.
What Are Property Hooks?
With property hooks, you can add custom actions whenever a property is accessed or changed, all within the property’s declaration. It’s basically a way to add getters and setters without writing them, like in C# and Swift.
Here's the canonical example everyone uses:
class User
{
public string $email {
set (string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address');
}
$this->email = strtolower($value);
}
}
}
$user = new User();
$user->email = 'John@Example.COM'; // Stored as 'john@example.com'
Looks nice, right? But let's dig deeper.
The Problem Nobody Asked to Solve
Were Getters and Setters Really That Bad?
For decades, PHP developers have been writing getters and setters:
class Product
{
private float $price;
public function getPrice(): float
{
return $this->price;
}
public function setPrice(float $price): void
{
if ($price < 0) {
throw new InvalidArgumentException('Price cannot be negative');
}
$this->price = $price;
}
}
Yes, it's verbose. Yes, it's boilerplate. But here’s the thing: It works. It’s easy to read, fix and every PHP developer can understand it. IDEs have been making these methods for us for quite some time. Was this the biggest problem that PHP should have been focused on?
The New Syntax: A Solution in Search of a Problem?
Now we can write:
class Product
{
public float $price {
set (float $value) {
if ($value < 0) {
throw new InvalidArgumentException('Price cannot be negative');
}
$this->price = $value;
}
}
}
We’ve reduced our code by... 4 lines? In exchange for making PHP developers learn a completely new syntax and for every tool in the ecosystem to support it.
The Hidden Complexity
Syntax Overload
PHP 8.4 doesn't just add simple get/set hooks. Oh no, we get a whole buffet of new syntax:
class KitchenSink
{
// Simple hooks
public string $simple {
get => $this->simple;
set (string $value) => $this->simple = $value;
}
// Asymmetric visibility (because apparently we needed this too)
public private(set) string $asymmetric;
// Reference hooks (wait, what?)
public array $refHook {
&get {
return $this->data;
}
}
// Virtual properties (properties that don't actually exist)
public string $virtual {
get => $this->computeSomething();
set (string $value) => $this->storeSomewhere($value);
}
}
All these variations have their own guidelines, unusual situations and chances for misunderstanding. The PHP manual page for this feature is going to be a novel.
The Principle of Least Surprise: Violated
Consider this code:
class Wallet
{
public float $balance = 100.0 {
set (float $value) {
echo "Balance changing from {$this->balance} to {$value}\n";
$this->balance = $value;
}
}
}
$wallet = new Wallet(); // What happens here?
Surprise! The first time 100.0 is assigned, the setter is not triggered. New developers will stumble on this. IExperienced developers will create bugs. The difference between initialization and assignment is a footgun waiting to happen.
Debugging Nightmares
Picture this scenario:
class Order
{
public float $total {
get {
$result = 0;
foreach ($this->items as $item) {
$result += $item->price * $item->quantity;
}
return $result * (1 + $this->taxRate);
}
}
}
// Somewhere in your code
$order->total; // Why is this slow?
You can easily add breakpoints, logging or profile certain methods using traditional debugging. Using property hooks makes debugging less straightforward. It becomes more difficult to understand stack traces. Performance issues become harder to track down.
Real-World Concerns
Performance: Not Free
Although PHP core developers say the performance hit is small, we should be honest about it.
// Direct property access
$obj->prop = 'value'; // Basically free
// Property with hook
$obj->prop = 'value'; // Function call overhead
In hot paths, this adds up. Sure, it's microseconds, but we've seen PHP applications brought to their knees by death by a thousand cuts. Why add another cut?
Backward Compatibility Theater
The migration story is messy:
class User
{
// New way
public string $email {
set (string $value) => $this->email = strtolower($value);
}
// But wait, we need to keep the old methods for BC
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
}
Now you have two ways to do the same thing. Which one is the "right" way? Do you deprecate the methods? Break your API? Leave both in place forever? Welcome to maintenance hell.
The Framework Fragmentation
Different frameworks will adopt property hooks at different rates and in different ways:
- Symfony will probably create a best practices guide that nobody follows
- Laravel will likely build some magical abstraction on top of it
- Legacy applications won't use them at all
- New projects will go overboard with them
The result? More inconsistency in the PHP ecosystem, not less.
What Other Languages Got Right (And PHP Didn't)
C#: Mature and Thoughtful
C# has had property accessors for decades:
public class Product
{
private decimal price;
public decimal Price
{
get { return price; }
set {
if (value < 0) throw new ArgumentException();
price = value;
}
}
}
But here's the difference: C# introduced these from the beginning. It's a core part of the language philosophy. PHP is adding this to a language that has been around for 30 years and follows many established conventions.
Python: Decorators > New Syntax
Python solved this elegantly with decorators:
class Product:
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price cannot be negative")
self._price = value
No new syntax. No BC breaks. A simple decorator pattern that works well with the language.
JavaScript: Keeping It Simple
JavaScript has had getters and setters for years:
class Product {
get price() { return this._price; }
set price(value) {
if (value < 0) throw new Error("Price cannot be negative");
this._price = value;
}
}
Clear, simple and without any extra complications. This could have been done in PHP, but instead we ended up with... whatever property hooks are trying to be.
The Elephant in the Room: Do We Need This?
Let’s admit the real reason property hooks are used:
- Because other languages have these features, PHP should have them as well
- Some of the main contributors really wanted this feature to be included
- Updating PHP’s appearance instead of addressing real problems
Meanwhile, PHP still has:
- Names for functions are not always the same (looking at you,
str_replace
versusstrpos
) - A library that is not organized
- Performance issues with large arrays
- No built-in support for async programming
- Type system holes you could drive a truck through
Of course, we can add property hooks. That’s what developers had been asking for.
When Property Hooks Might Actually Make Sense
To be fair, there are some legitimate use cases:
1. DTOs with Validation
class MoneyTransferDTO
{
public float $amount {
set (float $value) {
if ($value <= 0 || $value > 10000) {
throw new InvalidArgumentException('Invalid transfer amount');
}
$this->amount = $value;
}
}
}
On the other hand, you might choose to validate in the constructor or in the setter.
2. Legacy Code Modernization
If you're already using __get
and __set
magic methods, property hooks are a slight improvement:
// Old magic method approach
class Legacy
{
public function __set($name, $value)
{
// Unmaintainable mess
}
}
// Property hooks are marginally better
class LessLegacy
{
public string $prop {
set (string $value) => $this->prop = $value;
}
}
But if you're refactoring anyway, why not just use proper methods?
Best Practices (If You Must Use Them)
If you're determined to use property hooks, at least follow these guidelines:
1. Keep It Simple
// ❌ Don't do this
public float $complexProp {
get {
$result = $this->calculateBaseValue();
$result *= $this->applyMultipliers($result);
$result = $this->roundToNearestCent($result);
$this->logAccess('complexProp', $result);
return $result;
}
}
// ✓ Just use a method
public function getComplexProp(): float
{
// Same logic, but clearer intent
}
2. Don't Mix Patterns
Pick one approach per class:
// ❌ Confusing
class Mixed
{
public string $hookProp {
get => $this->hookProp;
}
private string $methodProp;
public function getMethodProp(): string
{
return $this->methodProp;
}
}
3. Avoid Hook Chains
// ❌ Debugging nightmare
class ChainedHooks
{
public float $a {
get => $this->b * 2;
}
public float $b {
get => $this->c + 10;
}
public float $c {
get => $this->calculateC();
}
}
Conclusion: A Feature in Search of a Problem
Property hooks in PHP 8.4 are not needed for any real problem. They add complexity to a language that's already struggling with its identity crisis. Are we a basic scripting language? A comprehensive platform for businesses? Because of property hooks, PHP seems to be open to anything which has led to a language that is a bit of a Frankenstein.
It’s not a bad feature, but it’s not needed. It addresses issues that have already been resolved, introduces new methods for making errors and requires developers to think harder. The effort put into property hooks could have been used to address real issues in PHP.
Will property hooks cause major problems in PHP? No. Will they improve it a lot? Also no. They’re just another part of a language that’s getting too big, something else to learn and another reason for code reviews to get stuck on details.
My advice? Always use getters and setters. They're boring, verbose, and they work. There are times when you want your production code to be as boring as possible. Use clever syntax only in languages that were made to support it.
The real question isn't whether property hooks change everything — it's whether everything needed changing in the first place.