Ganze Klassen unveränderlich machen
Seit PHP 8.1 lassen sich einzelne Properties als readonly deklarieren, seit PHP 8.2 kann eine komplette Klasse in einem Schritt unveränderlich gemacht werden. Der Unterschied ist mehr als Syntax-Zucker: Eine readonly Klasse trifft eine Design-Entscheidung auf Klassenebene, mit eigenen Regeln für Vererbung, dynamische Properties und Migration bestehender Value Objects.
Inhaltsverzeichnis
- 1. Abgrenzung zu einzelnen readonly Properties
- 2. Syntax: readonly vor der Klassendeklaration
- 3. Was sich automatisch ändert: alle Properties werden readonly
- 4. Keine dynamischen Properties mehr möglich
- 5. Vererbungsregeln bei readonly Klassen
- 6. Bestehende Value Objects zu readonly Klassen migrieren
- 7. Klonen und das with-Pattern in readonly Klassen
- 8. Wann readonly Klassen nicht passen
- 9. Tooling und Reflection bei readonly Klassen
- 10. Zusammenfassung
- 11. FAQ
1. Abgrenzung zu einzelnen readonly Properties
Einzelne readonly Properties sind ein eigenes Thema mit eigenen Regeln zu Initialisierung und Klonen, hier geht es bewusst nicht um diese Grundlagen, sondern um die Ebene darüber: die readonly Klasse als Ganzes. Statt jede Property einzeln mit dem Schlüsselwort readonly zu markieren, erklärt man mit PHP 8.2 die gesamte Klasse für unveränderlich, und der Interpreter wendet die Regel automatisch auf jede typisierte Property an.
Diese Verschiebung von der Property auf die Klasse ist kein reiner Schreibarbeits-Vorteil. Eine readonly Klasse ist eine Zusicherung an alle, die mit ihr arbeiten: Jede Instanz dieser Klasse ist nach der Konstruktion vollständig eingefroren, ohne Ausnahme für einzelne Felder. Das macht die Klasse zu einem klaren Signal für Value Objects und DTOs, bei denen partielle Veränderlichkeit ohnehin nie gewollt war.
2. Syntax: readonly vor der Klassendeklaration
Die Syntax platziert das Schlüsselwort readonly direkt vor class, wahlweise kombiniert mit final. Innerhalb der Klasse muss dann kein einziges readonly mehr vor den Properties stehen, das Schlüsselwort auf Klassenebene wirkt für jede typisierte Property, auch für über Constructor Property Promotion deklarierte.
Eine zentrale Voraussetzung wird dabei durchgesetzt: Jede Property der Klasse muss typisiert sein, denn readonly ohne Typ ist grundsätzlich ein Parse-Fehler, egal ob auf Property- oder Klassenebene deklariert. Wer eine bestehende Klasse mit untypisierten Properties zu readonly machen will, muss also zuerst vollständig typisieren.
declare(strict_types=1);
final readonly class Money
{
public function __construct(
private int $amountInCents,
private string $currency,
) {
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException('Currency mismatch');
}
// Returns a new instance, both operands stay untouched
return new self($this->amountInCents + $other->amountInCents, $this->currency);
}
}
3. Was sich automatisch ändert: alle Properties werden readonly
Sobald eine Klasse als readonly deklariert ist, gilt das Schlüsselwort implizit für jede Property, unabhängig davon, ob sie über einen klassischen Property-Block oder über Constructor Property Promotion definiert wurde. Ein zusätzliches readonly vor einer einzelnen Property ist danach nicht falsch, aber überflüssig, der Interpreter würde es ohnehin erzwingen.
Wird versucht, eine untypisierte Property innerhalb einer readonly Klasse zu deklarieren, bricht PHP den Vorgang mit einem Fatal Error zur Compile-Zeit ab, nicht erst zur Laufzeit beim ersten Zugriff. Das ist ein wichtiger Unterschied zur einzelnen readonly Property, bei der die restlichen Properties der Klasse ruhig untypisiert und mutable bleiben dürfen.
// Fatal error: Readonly property Config::$options must have type
readonly class Config
{
public $options; // untyped, not allowed in a readonly class
}
4. Keine dynamischen Properties mehr möglich
Eine readonly Klasse verbietet dynamische Properties vollständig, selbst dann, wenn die Klasse zusätzlich mit dem Attribut AllowDynamicProperties versehen wird. Der Grund liegt in der Natur dynamischer Properties: Sie werden zur Laufzeit ad hoc angelegt und wären damit nie durch die readonly-Prüfung des Deklarationszeitpunkts erfasst.
Für Legacy-Code, der sich auf freies Setzen beliebiger Properties verlässt, etwa als Ersatz für ein assoziatives Array, bedeutet die Migration zu readonly Klassen eine harte Grenze. Solcher Code muss vor der Umstellung entweder auf explizite, typisierte Properties oder auf ein separates Array-Feld umgestellt werden.
readonly class Options
{
public function __construct(public string $mode)
{
}
}
$options = new Options('strict');
$options->extra = 'value'; // Error: Cannot create dynamic property Options::$extra
5. Vererbungsregeln bei readonly Klassen
Erbt eine Klasse von einer readonly Klasse, muss die Kindklasse ebenfalls explizit readonly deklariert werden. PHP lässt keine stillschweigende Lockerung zu, bei der eine Unterklasse plötzlich mutable Properties einführt, während der Elternteil unveränderlich bleibt.
Lässt man das Schlüsselwort in der Kindklasse weg, bricht PHP die Deklaration mit einem Fatal Error ab, noch bevor überhaupt eine Instanz erzeugt wird. Das unterscheidet sich deutlich von der Vererbung bei einzelnen readonly Properties, wo eine Kindklasse frei neue, eigene readonly oder mutable Properties ergänzen kann, solange sie die geerbten Properties des Elternteils nicht anfasst.
readonly class Point
{
public function __construct(public float $x, public float $y)
{
}
}
// Fatal error: Class Point3D must be declared readonly to extend readonly class Point
class Point3D extends Point
{
public function __construct(float $x, float $y, public float $z)
{
parent::__construct($x, $y);
}
}
6. Bestehende Value Objects zu readonly Klassen migrieren
Bei der Migration eines gewachsenen Value Objects lohnt sich ein systematischer Blick auf jede Methode, die den Zustand verändert. Setter-Methoden, die eine Property direkt überschreiben, müssen restlos entfernt werden, denn ein einziger verbliebener Zuweisungspfad reicht aus, um beim ersten Aufruf einen Error auszulösen.
In der Praxis läuft die Migration meist in drei Schritten ab: Zuerst werden alle Properties vollständig typisiert, dann werden Setter durch wither-Methoden ersetzt, die eine neue Instanz zurückgeben, und erst zum Schluss wird das Schlüsselwort readonly vor die Klasse gesetzt. Diese Reihenfolge deckt bereits während der Umstellung auf, welche Aufrufer sich noch auf mutierenden Zustand verlassen.
// Before: mutable value object with a setter
final class DateRange
{
private DateTimeImmutable $start;
public function setStart(DateTimeImmutable $start): void
{
$this->start = $start; // mutates existing instance
}
}
// After: readonly class, wither instead of setter
final readonly class DateRange
{
public function __construct(
public DateTimeImmutable $start,
public DateTimeImmutable $end,
) {
}
public function withStart(DateTimeImmutable $start): self
{
return new self($start, $this->end); // returns a new instance
}
}
7. Klonen und das with-Pattern in readonly Klassen
Da eine readonly Klasse keine nachträgliche Zuweisung erlaubt, führt an einem wither-Pattern für abgeleitete Zustände kein Weg vorbei. Jede with-Methode konstruiert dabei intern eine komplett neue Instanz über den regulären Konstruktor, statt vorhandene Werte zu verändern.
Seit PHP 8.3 dürfen readonly Properties innerhalb der __clone-Methode einmalig neu zugewiesen werden, was bei sehr großen Objekten mit vielen Properties Konstruktor-Aufrufe sparen kann. Für die meisten Value Objects mit wenigen Feldern bleibt der explizite Aufruf des Konstruktors über new self jedoch der klarere und weniger fehleranfällige Weg.
8. Wann readonly Klassen nicht passen
Entities, deren Zustand über den gesamten Lebenszyklus einer Anfrage oder Session verändert wird, etwa ein Warenkorb oder ein von einem ORM verwaltetes Objekt, sind schlechte Kandidaten für readonly Klassen. Doctrine-Proxies zum Beispiel setzen Properties beim Lazy Loading nachträglich über Reflection, ein Vorgang, den eine readonly Klasse unterbindet.
Auch das Builder-Pattern, bei dem ein Objekt schrittweise über mehrere Methodenaufrufe aufgebaut wird, bevor es final verwendet wird, verträgt sich schlecht mit readonly Klassen, weil jeder Zwischenschritt eine neue Instanz statt einer Mutation des Builders erzeugen müsste. Für solche Fälle bleibt eine bewusst mutable Builder-Klasse, die am Ende ein unveränderliches Ergebnisobjekt liefert, die praktikablere Lösung. Die Faustregel lautet: readonly Klassen passen dort, wo Objekte sofort vollständig, mit allen benötigten Werten, entstehen, und nicht dort, wo Zustand über mehrere Schritte hinweg schrittweise aufgebaut werden muss.
9. Tooling und Reflection bei readonly Klassen
Statische Analyse-Werkzeuge wie PHPStan und Psalm erkennen Verstöße gegen readonly bereits zur Analysezeit und melden einen versuchten zweiten Schreibzugriff als Fehler, lange bevor der Code überhaupt ausgeführt wird. Für readonly Klassen gilt das genauso wie für einzelne readonly Properties, da beide über dasselbe Sprachfeature abgebildet werden.
Zur Laufzeit lässt sich der readonly-Status einer Klasse über ReflectionClass::isReadOnly() abfragen, was für generische Serialisierer oder Hydratoren nützlich ist, die zwischen veränderlichen und unveränderlichen Objekten unterscheiden müssen. Performancegewinne durch OPcache sind dabei ein Nebeneffekt, kein primärer Grund für den Einsatz von readonly Klassen.
$reflection = new ReflectionClass(Money::class);
if ($reflection->isReadOnly()) {
// Safe to share this instance across coroutines without defensive copying
echo 'Money is immutable';
}
| Aspekt | Einzelne readonly Property | readonly Klasse (PHP 8.2+) | Mutable Klasse |
|---|---|---|---|
| Deklarationsaufwand | Pro Property einzeln mit readonly |
Einmal auf Klassenebene für alle Properties | Kein zusätzliches Schlüsselwort nötig |
| Dynamische Properties | Für nicht-readonly Properties weiterhin erlaubt | Vollständig verboten, auch mit Attribut | Erlaubt, sofern nicht deaktiviert |
| Vererbung | Kindklasse frei bei eigenen Properties | Kindklasse muss ebenfalls readonly sein | Keine Einschränkung |
| Untypisierte Properties | Nicht erlaubt bei readonly Properties selbst | In der ganzen Klasse nicht erlaubt | Erlaubt |
| Migrationsaufwand | Gering, selektiv pro Feld | Mittel, betrifft die ganze Klasse | Kein Migrationsaufwand |
| Typischer Einsatzfall | Gemischte Klassen mit einzelnen festen Feldern | Reine Value Objects und DTOs | Entities mit veränderlichem Lebenszyklus |
Mironsoft
PHP-Modernisierung, Code-Qualität und Legacy-Refactoring
Gewachsener PHP-Code, der niemand mehr gern anfasst?
Wir modernisieren PHP-Codebasen auf aktuelle Sprachstandards, führen statische Analyse und Coding Standards ein und refactorn Legacy-Code Schritt für Schritt, ohne den laufenden Betrieb zu gefährden.
Legacy-Refactoring
Gewachsenen PHP-Code strukturiert und risikoarm modernisieren.
Code-Qualität etablieren
PHPStan, Coding Standards und CI-Checks nachhaltig im Team verankern.
Versions-Upgrade
PHP-Major-Version-Upgrades sicher planen und ohne Ausfallzeit umsetzen.
10. Zusammenfassung
Readonly Classes
Kernidee
readonly vor der Klasse macht jede typisierte Property automatisch unveränderlich, statt jede Property einzeln zu markieren.
Einschränkung
Keine dynamischen Properties, keine untypisierten Felder, Kindklassen müssen ebenfalls readonly sein.
Einsatzfall
Reine Value Objects und DTOs profitieren am meisten, Entities mit ORM-Lifecycle eher nicht.
Migration
Erst typisieren, dann Setter durch wither-Methoden ersetzen, erst danach readonly ergänzen.