In PHP 8.4, how do property hooks and virtual properties work, and why can't a readonly class expose a computed passengerName hook?
answer
- get and set bodies on a property
- backed if a hook uses $this->prop
- virtual: no storage, no default
- set param may accept a wider type
- hooked properties cannot be readonly
basics
~20 sSince PHP 8.4, get and set hooks attach logic to a property. A property whose hooks never touch its own storage is virtual and takes no memory. Hooked properties cannot be readonly, so readonly classes cannot declare hooks.
solid answer
~50 sA **property hook** (PHP 8.4) puts `get` and/or `set` logic on the property itself: `public string $email { set => strtolower($value); }`. Inside a hook, `$this->email` reaches the **backing value** directly, without re-entering the hook. If neither hook references the property itself, it is **virtual**: no storage, no default value allowed, and an omitted hook means that operation does not exist, so writing a get-only virtual property throws `Property ... is read-only`. That makes a computed field one line: `public string $passengerName { get => $this->firstName . ' ' . $this->lastName; }`. The catch in an immutable DTO: **hooked properties cannot be readonly**, and a `readonly class` makes every property readonly, so it fails with `Hooked properties cannot be readonly`. Keep the class non-readonly with `readonly` promoted fields plus the virtual hook, or use a method. Hooked properties also cannot be `unset()`, and `var_dump()`/`serialize()` see raw backing values.
code
php · 25 lines<?php
declare(strict_types=1);
final class FlightBooking // not a readonly class: hooks forbid it
{
public function __construct(
public readonly string $firstName,
public readonly string $lastName,
public readonly string $flightNo,
) {}
// virtual: never touches $this->passengerName, so no storage
public string $passengerName {
get => $this->firstName . ' ' . $this->lastName;
}
}
$b = new FlightBooking('Ada', 'Lovelace', 'BA117');
echo $b->passengerName, PHP_EOL; // Ada Lovelace
try {
$b->passengerName = 'X';
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL; // Property FlightBooking::$passengerName is read-only
}go deeper
Recall that PHP 8.4 lets a property declare get and set blocks that run on read and write.
Explain backed versus virtual properties, the short syntax, the wider set parameter type and why $this->prop inside a hook does not recurse.
Choose between hooks, asymmetric visibility and methods for real classes, and know the traps: no readonly, no unset, array writes, raw values in var_dump and serialize.
Weigh adopting hooks across a codebase against explicit methods, considering hidden costs on every access and interaction with immutability conventions.
## What a hook is A **property hook** is code that runs when a property is read (`get`) or written (`set`). Hooks arrived in **PHP 8.4** so that classes can expose plain properties and still add logic later, instead of writing `getX()`/`setX()` pairs up front. ```php public string $email { set => strtolower(trim($value)); // short set: the result is stored } ``` Syntax variants: - `get { ... return ...; }` or the short `get => expression;` - `set { $this->prop = ...; }` with an implicit `$value`, or `set(string|Stringable $value) { ... }` with an explicit parameter whose type may be the property type or **wider** - the short `set => expression;`, whose result becomes the stored value ## Backed versus virtual | | Backed property | Virtual property | |---|---|---| | how PHP decides | a hook references `$this->prop` by exact syntax | no hook references it | | storage in the object | yes | none | | default value | allowed | `Cannot specify default value for virtual hooked property` | | omitted hook | default read/write behaviour | the operation does not exist | Inside a hook, `$this->prop` accesses the backing value directly, so a `get` hook that returns `$this->prop` does not recurse. Accessing *other* properties from a hook goes through their hooks as usual. A virtual property with only `get` is a **computed, read-only field**. Writing it throws `Error: Property FlightBooking::$passengerName is read-only`. ## The flight-booking DTO An immutable booking DTO wants readonly data plus a derived `passengerName`. The natural first attempt fails: 1. `readonly class FlightBooking` makes every property readonly. 2. Hooked properties cannot be readonly. 3. Compilation stops with `Hooked properties cannot be readonly`, even for a get-only virtual property. The working shape is a **non-readonly** `final class` whose data properties are individually `readonly` (usually promoted) and whose computed field is a virtual hook. The object stays immutable in practice: the data cannot be rewritten and the virtual property has no `set`. ## Rules that bite - **No readonly**: `public readonly int $x { get; }` style declarations fail. Use asymmetric visibility (`public private(set)`) to restrict writes on a hooked property. - **No `unset()`**: `Cannot unset hooked property ...`. - **Array properties**: `$obj->items[] = $x` on a backed property with hooks throws `Indirect modification of ... is not allowed`, because modifying an element by reference would bypass the `set` hook. Replace the whole array instead, or declare a `&get` hook without `set`. - **Promotion**: hooks can be declared on promoted parameters, but the constructor parameter uses the property's type, not the wider `set` type. - **Static**: hooks exist only on instance properties. - **Inheritance**: a child may add or override individual hooks; `parent::$prop::get()` calls the parent's hook, and a hook may be declared `final`. ## Adding validation later without breaking callers The main promise of hooks is that a plain public property can gain behaviour without an API change: 1. Start with `public string $flightNo;` and let callers read and write it directly. 2. When a rule appears, add a hook to the same declaration: `set => strtoupper($value);`, or a block that throws `InvalidArgumentException` on a bad format. 3. Callers keep writing `$b->flightNo = 'ba117'`; nothing else changes. The exception is code that relied on references to that property or on writing its array elements, which hooks forbid, so search for `&$obj->prop` and element writes before adding a hook. ## What tools see The manual lists which operations go through hooks: `var_dump()`, `serialize()`, `unserialize()`, array casts and `get_mangled_object_vars()` use the **raw backing value**; `json_encode()`, `var_export()` and `get_object_vars()` read through the `get` hook. A hook that transforms values can therefore make `var_dump()` and `json_encode()` show different things. ## Hooks versus getter methods - **For hooks**: property syntax for callers, no boilerplate, validation added later without changing the API, computed fields that look like data. - **For methods**: explicit call cost visible at the call site, parameters, an obvious place for expensive work, and compatibility with readonly classes. - A hook runs on **every** access, so expensive work in `get` is hidden behind what looks like a field read. ## Summary Hooks attach `get`/`set` logic to a property; a property whose hooks never touch `$this->prop` is virtual and storage-free. Because hooked properties cannot be readonly, an immutable DTO with a computed field uses readonly promoted properties in a non-readonly class plus a get-only virtual hook.
- Inside a `get` hook, why doesn't `return $this->email;` call the hook again?Within a property's own hooks, `$this->email` refers to the backing value, not to the hooked property, so there is no recursion. That reference is also what makes the property backed; a hook that never uses it makes the property virtual.
- How do you restrict writes on a hooked property if readonly is not allowed?Use asymmetric visibility, for example `public private(set) string $email { set => strtolower($value); }`. Reads stay public, writes are limited to the class, and the `set` hook still runs for those writes. For a computed field, a virtual property with only a `get` hook has no write operation at all.
- Why does `$order->items[] = $item` throw on a backed property with a set hook?Writing an array element modifies the value through an implicit reference, which would bypass the `set` hook. PHP forbids it with `Indirect modification of ... is not allowed`. Assign a whole new array, or provide a method that builds it and assigns once.
saying these in an interview costs you the question
- A readonly class can declare a get-only virtual property as a computed field.
- Returning $this->prop inside its own get hook causes infinite recursion.
- A virtual property can have a default value.
- var_dump() always shows the value returned by the get hook.
- Hooks can be declared on static properties.