Readonly Properties (PHP 8.1)
debt(d2/e1/b3/t5)
Closest to 'caught instantly' (d1) but +1 because misuse (writing after init) throws a runtime Error rather than compile error; phpstan/psalm/rector flag readonly violations statically.
Closest to 'one-line patch' (e1) — quick_fix is literally adding the readonly keyword to a property declaration.
Closest to 'localised tax' (b3) — readonly applies per-property/per-class (value objects, DTOs); it shapes those classes but doesn't impose system-wide weight. Wither-method workaround adds some local friction.
Closest to 'notable trap' (t5) — misconception field states devs assume it's like a private setter, but it forbids reassignment even inside the class; also the cloning/wither gotcha is a documented surprise most devs learn.
Also Known As
TL;DR
Explanation
Readonly properties (PHP 8.1) must be typed and can only be written once — either in the constructor or at the point of declaration. Any subsequent write throws an Error. This is ideal for value objects and DTOs where properties should never change after construction. PHP 8.2 added readonly classes, where all promoted and explicitly declared properties become readonly automatically. Readonly properties cannot have a default value in their declaration (only in the constructor).
Common Misconception
Why It Matters
Common Mistakes
- Trying to reassign a readonly property even in the same class — any write after initialisation throws an Error.
- Not using constructor promotion with readonly — the combination is the primary intended usage.
- Using readonly on properties that need cloning with modifications — use a wither method returning a new instance.
- Declaring readonly on static properties — not supported; readonly applies to instance properties only.
Code Examples
// Mutable value object — can be corrupted after construction:
class Money {
public float $amount;
public string $currency;
}
$price = new Money();
$price->amount = 9.99;
$price->amount = -999; // Nothing prevents this mutation
// Readonly — immutable by language enforcement:
class Money {
public function __construct(
public readonly float $amount,
public readonly string $currency,
) {}
}
// PHP 8.1 — individual readonly properties
class User {
public function __construct(
public readonly int $id,
public readonly string $email,
) {}
// $user->id = 99; → Error: Cannot modify readonly property
}
// PHP 8.2 — readonly class (all promoted properties are readonly)
readonly class Money {
public function __construct(
public int $amount,
public string $currency,
) {}
// Methods can still return new instances
public function add(self $other): self {
return new self($this->amount + $other->amount, $this->currency);
}
}