skip to content

In PHP 8.5, what do readonly properties and readonly classes allow and forbid once an object is initialized?

level: middleimportance: must knowfreq 60%

answer

  1. one initializing write, then frozen
  2. must be typed, no default value
  3. implicitly protected(set) since 8.4
  4. the object inside is still mutable
  5. readonly class: every property, no dynamics

basics

~20 s

A 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 s

A `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
<?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-03

go deeper

for a junior

Recall that a readonly property can be assigned once and then never changed.

for a middle

Explain the declaration rules (typed, no default, not static), the kinds of modification that throw, and what a readonly class adds.

for a senior

Design immutable objects knowing readonly is shallow, that subclasses may initialize since 8.4, and that readonly classes exclude hooks and dynamic properties.

for a principal

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.