In PHP 8.5, what do readonly properties and readonly classes allow and forbid once an object is initialized?
answer
- one initializing write, then frozen
- must be typed, no default value
- implicitly protected(set) since 8.4
- the object inside is still mutable
- readonly class: every property, no dynamics
basics
~20 sA readonly property accepts exactly one initializing assignment, from its class or since 8.4 a subclass, then any write, unset or reference throws an Error. A readonly class makes every declared property readonly and forbids dynamic properties.
solid answer
~50 sA `readonly` property (PHP 8.1) can be **initialized once**; after that, assigning, incrementing, appending to an array, taking a reference or `unset()` all throw an `Error` such as `Cannot modify readonly property ...`. It must be **typed** (`mixed` if needed), cannot have a **default value**, and cannot be **static**. Since PHP 8.4 it is implicitly `protected(set)`, so the class and its subclasses may initialize it, while outside code attempting that first write gets `Cannot modify protected(set) readonly property ... from global scope`. Readonly is **shallow**: a readonly property holding a `DateTime` or another mutable object still lets callers mutate that object. A `readonly class` (PHP 8.2) marks every declared property readonly, rejects untyped and static properties, forbids dynamic properties and `#[\AllowDynamicProperties]`, and can only be extended by, or extend, another readonly class. Hooked properties cannot be readonly, so a readonly class cannot declare property hooks.
code
php · 22 lines<?php
declare(strict_types=1);
final class Itinerary
{
public function __construct(
public readonly string $pnr,
public readonly DateTime $departure,
) {}
}
$it = new Itinerary('X7K2LM', new DateTime('2026-11-02 07:30'));
try {
$it->pnr = 'Q1W2E3';
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL;
// Cannot modify readonly property Itinerary::$pnr
}
$it->departure->modify('+1 day'); // allowed: readonly is shallow
echo $it->departure->format('Y-m-d'), PHP_EOL; // 2026-11-03go deeper
Recall that a readonly property can be assigned once and then never changed.
Explain the declaration rules (typed, no default, not static), the kinds of modification that throw, and what a readonly class adds.
Design immutable objects knowing readonly is shallow, that subclasses may initialize since 8.4, and that readonly classes exclude hooks and dynamic properties.
Decide where immutability is enforced by the language versus convention, weighing readonly classes against the flexibility hooks and asymmetric visibility offer.
## Readonly properties (PHP 8.1) A **readonly property** can be written exactly once, by an *initializing* assignment. After that the value is fixed for the object's lifetime: ```php final class Fare { public function __construct(public readonly int $cents) {} } ``` Every kind of later modification throws an `Error`, usually `Cannot modify readonly property Fare::$cents`: - plain assignment, `+=`, `++` and `--`; - appending or writing an element when the property is an array (`$f->legs[] = ...`); - taking a reference (`$r = &$f->cents`) or passing it to a by-reference parameter; - `unset()` once it is initialized (`Cannot unset readonly property ...`). ## Declaration rules | Rule | Error when broken | |---|---| | must be typed (use `mixed` for "any") | `Readonly property Fare::$x must have type` | | no default value | `Readonly property Fare::$x cannot have default value` | | not static | `Static property Fare::$x cannot be readonly` | | no property hooks | `Hooked properties cannot be readonly` | The no-default rule follows from the one-write model: a default would be the only write ever allowed, which a class constant already expresses. ## Who may initialize it In PHP 8.1 to 8.3, only code in the **declaring class** could initialize a readonly property. Since **PHP 8.4**, readonly implies `protected(set)`, so methods of **subclasses** may initialize it as well. Code outside the hierarchy that attempts the initializing write gets `Cannot modify protected(set) readonly property Fare::$cents from global scope`; once the property is initialized, any write from anywhere fails with `Cannot modify readonly property Fare::$cents`. The set visibility can be written explicitly, for example `public private(set) readonly` to restore class-only initialization, or `public public(set) readonly` to allow a one-time write from anywhere. ## Readonly is shallow Readonly freezes the **property slot**, not the value's contents: - a readonly property holding an object stores a handle; the object behind it can still be mutated (`$booking->departure->modify('+1 day')` works on a mutable `DateTime`); - a readonly array cannot be changed, because arrays are values. For real immutability, store immutable types (`DateTimeImmutable`, enums, other readonly value objects) in readonly properties. ## Readonly classes (PHP 8.2) `readonly class Booking { ... }` is shorthand with extra guarantees: 1. every declared instance property is readonly, including promoted ones; 2. untyped properties are a compile error, and static properties are rejected; 3. **dynamic properties are forbidden** (`Cannot create dynamic property ...`), and `#[\AllowDynamicProperties]` cannot be applied; 4. inheritance must match: `Non-readonly class Bar cannot extend readonly class Foo`, and a readonly class cannot extend a non-readonly one; 5. because hooked properties cannot be readonly, a readonly class cannot declare **property hooks**, not even a get-only virtual property. ## Common interview traps 1. **Assuming readonly means an immutable object graph.** It freezes the slot only; store immutable types for deep immutability. 2. **Initializing from outside.** A factory function doing `$f = new Fare(); $f->cents = 100;` fails from global scope; initialize in the constructor or a class method. 3. **Appending to a readonly array.** `$fare->legs[] = $leg` throws even though no variable is reassigned; build the array first, then assign it once. 4. **Lazy initialization.** Leaving a readonly property unset and assigning it on first access works only from inside the class hierarchy, and only once. 5. **Converting a base class.** Parent and child must both be readonly, so making a widely extended base class readonly breaks every subclass. ## Cloning and withers A readonly object is changed by producing a modified **copy**. Since PHP 8.3 `__clone()` may reinitialize readonly properties, and PHP 8.5's `clone($obj, ['cents' => 900])` sets them on the copy directly; the rules for that belong to cloning. ## Summary Readonly means one initializing write, typed, no default, not static, no hooks, initializable by subclasses since 8.4, and shallow. A readonly class applies that to every property and also bans dynamic properties and mixed readonly/non-readonly inheritance.
- Why can't a readonly property have a default value?A readonly property allows exactly one initializing write. If the declaration supplied a default, that default would be the only value the property could ever hold, which is what a class constant is for. The engine rejects it with `Readonly property X::$y cannot have default value`; assign it in the constructor instead.
- Can a subclass initialize a readonly property declared in its parent in PHP 8.5?Yes, since PHP 8.4. Readonly properties are implicitly `protected(set)`, so a subclass method may perform the single initializing write. Declaring the property `private(set) readonly` restores the older rule that only the declaring class may initialize it.
saying these in an interview costs you the question
- A readonly property holding an object makes that object immutable too.
- Readonly properties can have a default value like any other property.
- An untyped property can be declared readonly.
- A readonly class can be extended by a normal class that adds mutable properties.
- Only the declaring class can ever initialize a readonly property, even in PHP 8.4.