Introduction
Property hooks, introduced in PHP 8.4, let you define custom get and set behavior directly on properties. They replace the traditional pattern of private properties with explicit getter/setter methods, similar to C# or Kotlin property accessors.
Key Concepts
- Property Hook: A
getorsetblock attached to a property declaration that runs when the property is read or written. - Virtual Property: A property with only a
gethook and no backing storage — its value is computed on every access. - Asymmetric Visibility: Different access levels for reading and writing a property, e.g.
public private(set)(PHP 8.4+).
Real World Context
In every PHP framework, you see entities with dozens of getX() and setX() methods that do little more than return or validate a value. Property hooks move that logic into the property declaration itself, making classes shorter and more declarative.
Deep Dive
Basic Syntax
Attach get and set hooks directly to a property:
php<?php class User { public string $name { get => strtoupper($this->name); set => trim($value); } } $user = new User(); $user->name = ' john doe '; // Calls set hook, stores 'john doe' echo $user->name; // Calls get hook, outputs 'JOHN DOE'
The set hook receives the incoming value as $value and the result is stored. The get hook transforms the stored value on read.
Virtual Properties
A property with only a get hook has no backing storage — it is computed every time:
php<?php class Rectangle { public function __construct( public float $width, public float $height ) {} public float $area { get => $this->width * $this->height; } public float $perimeter { get => 2 * ($this->width + $this->height); } } $rect = new Rectangle(10, 5); echo $rect->area; // 50 echo $rect->perimeter; // 30
Virtual properties look and feel like regular properties to the caller, but are computed on access.
Asymmetric Visibility (PHP 8.4)
Control read and write access independently:
php<?php class BankAccount { public private(set) float $balance = 0; public function deposit(float $amount): void { $this->balance += $amount; // OK - internal write } } $account = new BankAccount(); echo $account->balance; // OK - public read $account->balance = 100; // Error! Private set $account->deposit(100); // OK - internal modification
The public private(set) syntax means anyone can read the property, but only the class itself can write to it.
Validation in Set Hooks
Set hooks are ideal for validation:
php<?php class Product { public float $price { set { if ($value < 0) { throw new InvalidArgumentException('Price cannot be negative'); } $this->price = $value; } } public int $quantity { set(int $value) { $this->quantity = max(0, $value); } } }
Validation happens transparently whenever the property is assigned, keeping invariants enforced.
Temperature Converter Example
Property hooks enable elegant bidirectional computed properties:
php<?php class Temperature { private float $celsius; public float $fahrenheit { get => $this->celsius * 9/5 + 32; set { $this->celsius = ($value - 32) * 5/9; } } public function __construct(float $celsius) { $this->celsius = $celsius; } } $temp = new Temperature(0); echo $temp->fahrenheit; // 32 $temp->fahrenheit = 212; // Sets celsius to 100
Reading fahrenheit converts from celsius; writing fahrenheit converts back. The backing store is always celsius.
Interface Properties with Hooks
Interfaces can declare property signatures:
php<?php interface HasFullName { public string $fullName { get; } } class Person implements HasFullName { public function __construct( public string $firstName, public string $lastName ) {} public string $fullName { get => "$this->firstName $this->lastName"; } }
This lets interfaces require computed properties without dictating the implementation.
Common Pitfalls
- Infinite recursion in get hooks — Accessing
$this->nameinside thegethook of$nametriggers the hook again. Use a backing property or the implicit storage to avoid loops. - Forgetting explicit assignment in set hooks — When using the block
set { ... }syntax (not the arrow syntax), you must explicitly assign$this->property = $value;or the value is not stored.
Best Practices
- Use virtual properties for computed values — Replace
getArea()methods with virtual properties for a cleaner API. - Use asymmetric visibility instead of getters —
public private(set)is more idiomatic than a private property with a public getter method.
Summary
- Property hooks attach
getandsetlogic directly to property declarations. - Virtual properties have only a
gethook and no backing storage. - Asymmetric visibility (
public private(set)) controls read/write access independently. - Set hooks are ideal for validation and normalization.
- Interface properties can require hooks, enabling contract-driven computed properties.
Code Examples
<?php
declare(strict_types=1);
class Order {
private array $items = [];
public private(set) string $status = 'pending';
public string $customerEmail {
set {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
$this->customerEmail = strtolower($value);
}
}
public float $total {
get => array_sum(array_map(
fn($item) => $item['price'] * $item['quantity'],
$this->items
));
}
public string $formattedTotal {
get => '$' . number_format($this->total, 2);
}
public function addItem(string $name, float $price, int $quantity): void {
$this->items[] = compact('name', 'price', 'quantity');
}
public function markShipped(): void {
$this->status = 'shipped';
}
}
$order = new Order();
$order->customerEmail = 'JOHN@EXAMPLE.COM';
$order->addItem('Widget', 9.99, 2);
$order->addItem('Gadget', 19.99, 1);
echo $order->formattedTotal; // $39.97
echo $order->status; // pending
?>