readonly Classes (PHP 8.2)
debt(d5/e3/b3/t5)
Closest to 'specialist tool catches it' (d5). The detection_hints list rector and phpstan — both specialist static-analysis tools — as the primary means of finding misuse (e.g. attempting to extend a non-readonly class, adding static properties, or misunderstanding shallow immutability). These issues won't be flagged by a default linter but will be caught by phpstan type-checking or rector migrations.
Closest to 'simple parameterised fix' (e3). The quick_fix instructs converting value objects/DTOs to readonly classes and removing individual readonly keywords — a straightforward, pattern-based refactor typically contained within one or a small set of related files. It's more than a one-line patch but doesn't span the whole codebase.
Closest to 'localised tax' (b3). The applies_to scope covers web, cli, and queue-worker contexts, but the choice is confined to the specific value objects and DTOs that use readonly classes. The rest of the codebase is unaffected; it's a localised structural choice per class, not a gravitational architectural decision.
Closest to 'notable trap' (t5). The misconception field explicitly states developers assume deep immutability — but readonly only prevents property reassignment, not mutation of nested objects (shallow immutability). This is a well-documented gotcha that many developers eventually discover, plus the additional surprises of inability to extend non-readonly classes and no static properties.
TL;DR
Explanation
readonly class Money { } makes all declared properties implicitly readonly — no need to mark each one. Rules: all properties must be typed. Cannot have untyped or static properties. Allows constructor promotion. Can extend readonly classes. Cannot extend non-readonly classes (or vice versa — asymmetric readonly inheritance breaks the contract). readonly classes implement all readonly property restrictions: write-once, no unset, requires type. PHP 8.3: readonly properties can be modified in __clone() for copy-with patterns. Ideal for: DTOs, value objects, request/response objects, command/query objects.
Common Misconception
Why It Matters
Common Mistakes
- Trying to extend a non-readonly class from readonly class — fatal error.
- Adding static properties to readonly class — not allowed.
- Assuming deep immutability — nested objects are still mutable.
Code Examples
class UserDTO {
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $email,
) {}
}
// PHP 8.2 — readonly class:
readonly class UserDTO {
public function __construct(
public int $id,
public string $name,
public string $email,
) {}
// PHP 8.3 copy-with:
public function withName(string $name): static {
$clone = clone $this;
$clone->name = $name; // Only in __clone in 8.3
return $clone;
}
}