PHP 8.4 is coming: property hooks and asymmetric visibility
Property hooks and asymmetric visibility in PHP 8.4, and how these additions will change the way we model classes.
PHP 8.4 is releasing at the end of this month. I’m already looking at it on beta before the stable release, because 8.4 is one of the most noteworthy versions in recent years: it brings two additions that will genuinely change the way we model class properties.
Property hooks
Until now, whenever we wanted to intercept a property being read or written, we’d write getter/setter methods. This turned even a simple field into a pair of methods and bloated the class.
PHP 8.4 introduces property hooks: you can attach logic directly to a property that runs when it is read or written:
class Kullanici
{
public string $tamAd {
get => $this->ad . ' ' . $this->soyad;
}
public string $eposta {
set => strtolower(trim($value));
}
public function __construct(
private string $ad,
private string $soyad,
) {}
}
Here $tamAd is never stored; it is computed on read. $eposta is normalized on write. Code from the outside still sees a plain property — $kullanici->eposta — but logic can be injected. Something I used to solve with getter/setter pairs is now expressed by the property itself.
Hooks working alongside interfaces
The area where property hooks will make the biggest difference is interfaces. Hook signatures can be declared in interfaces:
interface Adlandirilabilir
{
public string $tamAd { get; }
}
This moves the contract — “this class must have a readable $tamAd property” — directly into the interface. Whether the implementing class stores a real field or computes it via a get hook is irrelevant, as long as the contract is fulfilled. I used to handle this with a doc comment or by forcing a getName() method; now the language understands it.
A pitfall to watch out for
The set hook does not write the value to the property — that is your responsibility. If you don’t write $this->eposta = $value inside the hook, the property is never assigned. This can catch someone coming from C# or Kotlin properties off-guard:
public string $eposta {
set {
// Normalize AND don't forget to STORE:
$this->eposta = strtolower(trim($value));
}
}
With the short arrow syntax (set => strtolower(trim($value))), the assignment is automatic; the explicit write is only required when you use the block form { set { ... } }.
Asymmetric visibility
The second addition is asymmetric visibility. You can bind separate visibility levels to reading and writing a property:
class Siparis
{
public private(set) string $durum = 'bekliyor';
}
Here $durum is readable from outside but can only be changed from within the class. Previously you had to make the property private and write a separate getter method for this. Now the intent — “everyone can see it, nobody can change it from outside” — is expressed in a single line.
protected(set) is also available, meaning subclasses can write but it is closed to the outside:
class Temel
{
public protected(set) int $versiyon = 1;
}
class Alt extends Temel
{
public function yuksel(): void
{
$this->versiyon++; // Valid
}
}
$obj = new Alt();
echo $obj->versiyon; // 1 — readable
$obj->versiyon = 5; // Error! Cannot write from outside
This is the more flexible sibling of the readonly properties introduced in PHP 8.1. readonly can be written once and never changed again — truly immutable. Asymmetric visibility gives the guarantee of “write from inside as many times as you like, nobody outside can touch it.” The two address different needs.
What these two features say together
Property hooks and asymmetric visibility point in the same direction: keep using properties like simple properties, but retain control when you need it. For years, whenever I wanted to protect or compute a field I had to bloat the class with methods; calling code then saw methods instead of plain fields. 8.4 removes that trade-off.
I feel this most clearly when writing value objects. Previously in a class like Money I’d write chains of getters: getAmount(), getCurrency(), getFormatted(). Now it’s $money->amount, $money->currency, $money->formatted — no methods to read, but control in place for writes.
When the release lands, the first thing I’ll do is revisit the pile of getters in my value objects. Most of them will collapse into a simple property hook. Less code, same guarantee — that’s exactly what I want from a language release.
Comparison with readonly
It’s worth drawing a clear distinction between readonly properties from PHP 8.1 and asymmetric visibility with property hooks. A readonly property can only be assigned once — outside the constructor — and can never be changed afterwards; it is fully immutable. private(set) with asymmetric visibility gives the guarantee of “change it from inside as many times as you like, but nobody outside can touch it.” The two serve different needs: readonly is better suited for value objects, while private(set) is the right tool when you want state to be mutable only from within the class in a controlled way. Using one in place of the other without understanding this distinction will back you into a corner the moment a change is required.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.