Property Hooks (PHP 8.4)
What Are Property Hooks? In simple terms: Property hooks let you attach small pieces of code to a property that run automatically when the property is read or written . You can normalize an email addr
What Are Property Hooks?
In simple terms: Property hooks let you attach small pieces of code to a property that run automatically when the property is read or written. You can normalize an email address whenever it is assigned, reject invalid prices, or provide a calculated property such as fullName — while code using the object still writes simple property access like $user->email = '...'.
Technically: PHP 8.4 introduced get and set hooks declared in a block after a property declaration. A set hook runs on assignment and receives the new value; a get hook runs on read and returns the value. Inside a hook, $this->propertyName accesses the property's underlying backing value. A property whose hooks never use the backing value is virtual: it stores nothing and is computed on access.
Why Hooks Were Added
Before PHP 8.4, validating or computing property values required one of these approaches:
| Approach | Drawback |
|---|---|
Private property + getEmail() / setEmail() methods | Boilerplate for every property; callers must use methods |
Magic __get() / __set() | Slow, invisible to IDEs and static analysis, easy to get wrong |
| Public property without checks | No validation at all |
Hooks keep the simple syntax of public properties while adding the control of methods. A class can start with a plain public property and add a hook later without changing any calling code.
A set Hook: Validation and Normalization
<?php
final class User
{
public string $email {
set(string $value) {
$value = strtolower(trim($value));
if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
throw new InvalidArgumentException("Invalid email: $value");
}
$this->email = $value;
}
}
public function __construct(public string $name, string $email)
{
$this->email = $email; // runs the set hook
}
}
$user = new User('Asha', ' Asha@Example.COM ');
echo $user->email . "\n";
$user->email = 'asha.k@gocourse.in';
echo $user->email . "\n";
try {
$user->email = 'not-an-email';
} catch (InvalidArgumentException $e) {
echo $e->getMessage() . "\n";
}
echo $user->email . "\n";
Output
asha@example.com
asha.k@gocourse.in
Invalid email: not-an-email
asha.k@gocourse.in
How It Works
public string $email { set(...) { ... } }declares a public property with asethook.- Every assignment — in the constructor or from outside — calls the hook with the new value as
$value. - The hook normalizes the value (trim, lowercase) and validates it. Invalid values throw an exception, so the property keeps its previous valid value.
$this->email = $value;inside the hook writes the backing value directly; it does not call the hook again.- Reading
$user->emailhas nogethook, so it simply returns the stored value.
The set hook's parameter list can be omitted; the new value is then available as $value automatically: set { ... }.
Short Hook Syntax
For simple transformations, the arrow form is more concise:
<?php
final class Product
{
public string $sku {
set => strtoupper(trim($value)); // the result is stored
}
public function __construct(string $sku)
{
$this->sku = $sku;
}
}
$product = new Product(' pen-01 ');
echo $product->sku . "\n";
Output
PEN-01
set => expression; stores the result of the expression in the backing value. Similarly, get => expression; returns the result of an expression.
get Hooks and Virtual Properties
A get hook that does not use the property's own backing value creates a virtual property — a computed value that takes no storage:
<?php
final class Product
{
public int $pricePaise {
set {
if ($value < 0) {
throw new InvalidArgumentException('Price cannot be negative.');
}
$this->pricePaise = $value;
}
}
public string $formattedPrice {
get => '₹' . number_format($this->pricePaise / 100, 2);
}
public function __construct(public string $name, int $pricePaise)
{
$this->pricePaise = $pricePaise;
}
}
$lamp = new Product('Desk Lamp', 129900);
echo $lamp->formattedPrice . "\n";
$lamp->pricePaise = 99900;
echo $lamp->formattedPrice . "\n";
try {
$lamp->formattedPrice = '₹1';
} catch (Error $e) {
echo $e->getMessage() . "\n";
}
Output
₹1,299.00
₹999.00
Property Product::$formattedPrice is read-only
formattedPriceis calculated frompricePaiseon every read, so it can never be out of date.- It has only a
gethook and no backing value, so it cannot be written; assignment throws anError. - Virtual properties appear like normal properties to callers, which is convenient in templates and serializers:
{$product->formattedPrice}.
Combining get and set
Both hooks can be defined on one property:
public string $phone {
get => '+91 ' . substr($this->phone, 0, 5) . ' ' . substr($this->phone, 5);
set => preg_replace('/\D+/', '', $value);
}
The phone number is stored as digits only and displayed formatted.
Hooks in Interfaces and Abstract Classes
PHP 8.4 also allows properties in interfaces, declaring which operations implementations must support:
<?php
interface HasDisplayName
{
public string $displayName { get; }
}
final class Customer implements HasDisplayName
{
public function __construct(private string $firstName, private string $lastName) {}
public string $displayName {
get => "{$this->firstName} {$this->lastName}";
}
}
final class Company implements HasDisplayName
{
public function __construct(public string $displayName) {}
}
foreach ([new Customer('Asha', 'Kumar'), new Company('GoCourse Pvt Ltd')] as $party) {
echo $party->displayName . "\n";
}
Output
Asha Kumar
GoCourse Pvt Ltd
{ get; } requires a readable property. Customer provides it as a virtual property; Company uses a plain public property (here, promoted). Code that depends on HasDisplayName works with both. Abstract classes can similarly declare abstract properties with hook requirements.
Rules and Limitations
| Rule | Detail |
|---|---|
Hooks and readonly cannot be combined | Use asymmetric visibility or a set hook instead (see Asymmetric Visibility (PHP 8.4)) |
| Static properties cannot have hooks | Hooks apply to object properties only |
The set parameter type must accept the property type | It may be wider, allowing conversion inside the hook |
| Virtual properties have no storage | They cannot be written unless a set hook is defined |
| Promoted constructor properties can have hooks | public function __construct(public string $sku { set => strtoupper($value); }) |
| Indirect modification of hooked array properties | $obj->items[] = $x is not allowed for properties with a set hook |
Hooks vs Methods
| Use a Hook When | Use a Method When |
|---|---|
| The operation is a natural property assignment or read | The operation is an action with business meaning (markAsPaid()) |
| Logic is short: normalization, validation, simple computation | Logic is complex, slow, or has side effects (database, network) |
| You want to add validation to an existing public property without changing callers | The operation needs several parameters |
Property access should remain cheap and free of surprising side effects; heavy work belongs in methods.
Common Mistakes
Assigning Through the Property Inside Its Own Hook Expecting Recursion
Effect: Developers sometimes think $this->email = $value inside the set hook calls the hook again.
Fix: Understand that inside its own hooks, $this->property accesses the backing value directly.
Expensive get Hooks
Effect: Every read repeats a slow computation.
Fix: Keep hooks fast, or use explicit methods with caching.
Combining Hooks With readonly
Effect: Compile error.
Fix: Use public private(set) with a set hook, or a readonly property without hooks.
Putting Business Workflows in Hooks
Effect: Hidden side effects on simple assignments.
Fix: Use named methods for actions.
Best Practices
- Use
sethooks to validate and normalize property values at the moment they are assigned. - Use
get-only virtual properties for derived values. - Declare required properties in interfaces with
{ get; }or{ get; set; }. - Keep hooks short, fast, and free of side effects.
- Start with plain public properties and add hooks when rules appear — without changing calling code.
Official reference: https\://www.php.net/manual/en/language.oop5.property-hooks.php