Readonly Classes in PHP 8.2: Ganze Klassen unveränderlich machen
AI generated
8.4
PHP · Readonly Classes · PHP 8.2
Readonly Classes in PHP 8.2
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.

11 Min. Lesezeit Value Objects PHP 8.2 - 8.4

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.

11. FAQ: Readonly Classes

1Was ist der Unterschied zwischen readonly Properties und readonly Klassen?
Bei einzelnen readonly Properties wird jedes Feld separat markiert, bei einer readonly Klasse gilt die Unveränderlichkeit automatisch für alle typisierten Properties der Klasse auf einmal.
2Seit welcher PHP Version gibt es readonly Klassen?
Readonly Klassen wurden mit PHP 8.2 eingeführt, einzelne readonly Properties existieren bereits seit PHP 8.1.
3Kann eine readonly Klasse dynamische Properties haben?
Nein, dynamische Properties sind in readonly Klassen vollständig verboten, auch das Attribut AllowDynamicProperties ändert daran nichts.
4Müssen alle Properties einer readonly Klasse typisiert sein?
Ja, jede Property muss einen Typ deklarieren, eine untypisierte Property führt in einer readonly Klasse zu einem Fatal Error.
5Muss eine Kindklasse einer readonly Klasse auch readonly sein?
Ja, PHP erzwingt, dass jede Kindklasse einer readonly Klasse ebenfalls explizit als readonly deklariert wird, sonst schlägt die Deklaration fehl.
6Kann man eine readonly Property in einer readonly Klasse trotzdem beim Klonen ändern?
Seit PHP 8.3 ist eine einmalige Neuzuweisung innerhalb der __clone-Methode erlaubt, außerhalb davon bleibt jede Property nach der Erstzuweisung fest.
7Eignen sich readonly Klassen für Doctrine Entities?
In der Regel nicht, da Doctrine Properties beim Lazy Loading nachträglich über Reflection setzt, was eine readonly Klasse verhindert.
8Wie migriert man bestehende Value Objects zu readonly Klassen?
Zuerst alle Properties typisieren, dann Setter durch wither-Methoden ersetzen, die eine neue Instanz zurückgeben, und erst danach das Schlüsselwort readonly ergänzen.
9Bringt eine readonly Klasse messbare Performance-Vorteile?
Mögliche OPcache-Optimierungen sind ein Nebeneffekt, der eigentliche Nutzen liegt in der Korrektheit und der einfacheren gefahrlosen gemeinsamen Nutzung von Instanzen.
10Wie lässt sich zur Laufzeit prüfen, ob eine Klasse readonly ist?
Über ReflectionClass::isReadOnly() lässt sich der readonly-Status einer Klasse zur Laufzeit abfragen, etwa für generische Serialisierer.