PHP 8.4 Property Hooks: The Feature That Changes Everything

The Boilerplate Problem
For twenty years, PHP developers have written the same ritual: make the property private, write a getter, write a setter, repeat for every field. Not because the design demanded it — but because it was the only way to reserve the right to add validation or transformation later without breaking every caller.
// The old way — 15 lines to guard one string
class User
{
private string $email;
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: {$email}");
}
$this->email = strtolower($email);
}
}
PHP 8.4's property hooks (from the property hooks RFC) end that ritual. The logic moves onto the property itself, and callers use plain property syntax:
// The 8.4 way
class User
{
public string $email {
set {
if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: {$value}");
}
$this->email = strtolower($value);
}
}
}
$user = new User();
$user->email = '[email protected]'; // stored as '[email protected]'
$user->email = 'not-an-email'; // throws InvalidArgumentException
Inside a set hook, the incoming value is available as $value automatically. No getter, no setter, no private backing field to manage — and if a class never needs hooks, it never pays for them. You can add a hook to a plain public property years later without breaking a single caller. That's the part that changes everything: getters-and-setters-just-in-case is no longer a defensible default.
The Four Hook Forms
Both hooks come in a block form and a short arrow form:
class Post
{
// 1. get block — full body, return required
public string $preview {
get {
return substr(strip_tags($this->content), 0, 160);
}
}
// 2. get arrow — single expression
public int $wordCount {
get => str_word_count(strip_tags($this->content));
}
// 3. set block — you assign the backing value yourself
public string $title {
set {
$this->title = trim($value);
}
}
// 4. set arrow — the expression result IS the stored value
public string $slug {
set => strtolower(str_replace(' ', '-', trim($value)));
}
public string $content = '';
}
The arrow set form trips people up the first time: set => expr doesn't return in the usual sense — whatever the expression evaluates to is what gets written to the property. It's the concise form for normalisation (trimming, lowercasing, slugifying).
Virtual Properties: Computed Values with Property Syntax
If a get hook never touches the property's own backing value, PHP allocates no storage at all. The property is virtual — computed on every read:
class Invoice
{
public function __construct(
public int $amountCents,
public string $currency = 'AED',
) {}
// No stored value — recomputed on each access
public string $formattedTotal {
get => sprintf('%s %.2f', $this->currency, $this->amountCents / 100);
}
}
$invoice = new Invoice(45900);
echo $invoice->formattedTotal; // "AED 459.00"
Writing to a virtual property with no set hook throws an Error — exactly what you want for derived values.
Asymmetric Visibility: The Other Half of the Story
PHP 8.4 shipped a second, closely related feature (asymmetric visibility RFC): a property can now have a different visibility for reading and writing.
class Order
{
public private(set) string $status = 'pending';
public function markPaid(): void
{
$this->status = 'paid'; // fine — inside the class
}
}
$order = new Order();
echo $order->status; // fine — public read
$order->status = 'paid'; // Error: cannot modify private(set) property
Before 8.4 this required a private property plus a public getter. Now it's one declaration. protected(set) exists too, and you can combine visibility with hooks on the same property. Together, hooks and asymmetric visibility eliminate the two remaining reasons getter/setter pairs existed: transformation and write-protection.
Hooks in Interfaces
Interfaces can now demand properties, not just methods:
interface Sluggable
{
public string $slug { get; }
}
class Article implements Sluggable
{
public function __construct(public string $title) {}
public string $slug {
get => strtolower(str_replace(' ', '-', $this->title));
}
}
Any class satisfies { get; } with either a hooked property or a plain public one. API contracts that used to force getSlug(): string methods can now speak in properties.
A Real Example: Self-Validating DTOs
Where hooks genuinely shine is data transfer objects. Here's a pattern from a ride-booking API — coordinates that are impossible to construct invalid:
final class FareEstimateRequest
{
public float $lat {
set {
if ($value < -90.0 || $value > 90.0) {
throw new InvalidArgumentException("Latitude {$value} is out of range.");
}
$this->lat = $value;
}
}
public float $lng {
set {
if ($value < -180.0 || $value > 180.0) {
throw new InvalidArgumentException("Longitude {$value} is out of range.");
}
$this->lng = $value;
}
}
}
Every service downstream can trust the object without re-validating. The invariant lives in exactly one place — on the property it protects.
The Laravel Caveat: Hooks Don't Work on Eloquent Attributes
This is the mistake every Laravel developer will make once, so let's make it now, in writing:
// This does NOT do what you hope
class Post extends Model
{
public string $title {
set => ucfirst($value); // breaks Eloquent persistence!
}
}
Eloquent attributes aren't real properties — they live in the model's internal $attributes array and are reached through __get/__set magic. Declaring a real (hooked) property with the same name shadows the attribute: your value never reaches $attributes, never gets saved, never appears in toArray().
The rule of thumb:
- On models — keep using casts and
Attributeaccessors/mutators. That's the Eloquent-native mechanism. - Everywhere else — DTOs, value objects, form objects, services, enums-with-state — property hooks are now the cleanest tool in the box.
Gotchas Worth Knowing
readonlyand hooks don't mix. A hooked property can't be declaredreadonly. If you want write-once semantics with public reads, useprivate(set)instead.- References need opt-in. You can't take a reference to a hooked property (or
$obj->arr[] = ...through one) unless the class declares an explicit&gethook. - Performance is a non-issue. Properties without hooks are exactly as fast as before, and the engine inlines simple hooks. Don't let micro-benchmark anxiety keep you on getter pairs.
What Else Shipped in PHP 8.4
Property hooks headline the release, but 8.4 is dense:
array_find(),array_any(),array_all(),array_find_key()— the array searches you've been writing witharray_filter(...)[0] ?? nullfor years:$firstAdmin = array_find($users, fn ($u) => $u->isAdmin());newwithout wrapping parentheses — chain straight off instantiation:$slug = new Slugifier()->slugify($title); // was: (new Slugifier())->...- An HTML5-compliant DOM parser —
Dom\HTMLDocument::createFromString()finally parses real-world HTML without the libxml warnings ritual. - Lazy objects — native proxy/ghost object support via Reflection, which container and ORM authors (Doctrine, Symfony) are already using.
#[\Deprecated]attribute — deprecate your own methods and constants with engine-level warnings.mb_trim()and friends, and aBcMath\Numberobject API with operator support.- Heads-up: implicitly nullable parameters (
function f(Foo $x = null)) are deprecated — write?Foo $x = nullbefore 9.0 makes it fatal.
Should You Use Hooks Today?
Yes — with the Eloquent caveat above. New DTOs and value objects should use typed public properties, adding hooks only where an invariant or transformation exists, and private(set) where callers may read but not write. Existing code doesn't need a rewrite: the beauty of hooks is that you can introduce them property-by-property, later, without breaking call sites.
If you want to see typed DTOs carrying data through a real API, that's exactly how the request flow works in Build a REST API with Laravel 13 and Sanctum — and how every module is structured in the modular monolith that powers our ride-booking platform.
Questions or a different take? Drop a comment — I read every one.
Frequently Asked Questions
What are property hooks in PHP 8.4?
Do PHP 8.4 property hooks work with Laravel Eloquent models?
Can property hooks be combined with readonly?
Senior Full Stack Developer · Building SaaS products & teaching Laravel/React · 10+ years experience · Founder of Orion360 · Based in Dubai, UAE.
Was this post helpful?
Reviews & Ratings
Sign in to leave a review.



