MWCodebymw.de ↗
PHP

readonly und Constructor Promotion – Wertobjekte in PHP ohne Boilerplate

Früher waren das dreißig Zeilen: Eigenschaften deklarieren, Konstruktor schreiben, alles zuweisen, Getter dazu. Seit PHP 8.1 ist derselbe unveränderliche Wertetyp eine einzige Zeile – und niemand kann ihn nachträglich kaputt machen.

So sah eine kleine Datenklasse in PHP lange aus:

class Adresse
{
    private string $strasse;
    private string $plz;
    private string $ort;

    public function __construct(string $strasse, string $plz, string $ort)
    {
        $this->strasse = $strasse;
        $this->plz = $plz;
        $this->ort = $ort;
    }

    public function getStrasse(): string { return $this->strasse; }
    public function getPlz(): string { return $this->plz; }
    public function getOrt(): string { return $this->ort; }
}

Zwanzig Zeilen, in denen jeder Feldname dreimal steht. Seit PHP 8.1 ist dasselbe:

final class Adresse
{
    public function __construct(
        public readonly string $strasse,
        public readonly string $plz,
        public readonly string $ort,
    ) {}
}

Zwei Dinge passieren hier gleichzeitig.

Constructor Property Promotion

public string $strasse im Konstruktor deklariert die Eigenschaft und weist sie zu, in einem Schritt. Das ist reine Schreibarbeit, die wegfällt — die Klasse verhält sich exakt wie vorher.

readonly

Das ist der interessantere Teil. Eine readonly-Eigenschaft darf genau einmal gesetzt werden, und zwar aus der Klasse selbst heraus. Danach ist sie fest:

$a = new Adresse('Hauptstr. 1', '90402', 'Nürnberg');
echo $a->ort;          // Nürnberg
$a->ort = 'Fürth';     // Error: Cannot modify readonly property

Kein privates Feld plus Getter mehr nötig, nur um Schreibzugriff zu verhindern. Die Eigenschaft darf public sein — lesen ist erlaubt, ändern nicht.

Warum mir das wirklich hilft

Der Gewinn ist nicht die gesparte Tipparbeit, sondern das, was danach nicht mehr passieren kann. Ein Objekt, das man nach dem Erzeugen nicht mehr verändern kann, hat keinen Zustand, den irgendein anderer Teil des Codes heimlich umbiegt. Wenn ich eine Adresse in eine Funktion gebe, weiß ich: Was zurückkommt, ist noch dieselbe Adresse.

Genau deshalb setze ich das bei allem ein, was einen Wert darstellt und nicht ein veränderliches Ding: Geldbeträge, Zeiträume, Konfigurationen, API-Antworten, Zeilen aus einer Datenbank auf dem Weg in die Ausgabe.

Ändern heißt: ein neues Objekt

Wenn man doch etwas ändern will, baut man eine Kopie:

final class Adresse
{
    public function __construct(
        public readonly string $strasse,
        public readonly string $plz,
        public readonly string $ort,
    ) {}

    public function mitOrt(string $ort): self
    {
        return new self($this->strasse, $this->plz, $ort);
    }
}

$neu = $alt->mitOrt('Fürth');   // $alt bleibt unangetastet

Das ist derselbe Gedanke wie DateTimeImmutable gegenüber DateTime: Nicht das Original verändern, sondern ein neues Ergebnis liefern.

Zwei Stolpersteine

  • readonly gilt nicht in die Tiefe. Ist die Eigenschaft ein Array, kannst du das Array nicht austauschen — aber ist sie ein Objekt, lässt sich dessen Inneres weiterhin verändern. Wenn es wirklich fest sein soll, müssen auch die enthaltenen Objekte readonly sein.
  • Ein Standardwert ist nicht erlaubt. public readonly string $ort = 'Nürnberg' geht nicht — der Wert muss im Konstruktor kommen. Standardwerte setzt man deshalb am Parameter.

Seit PHP 8.2 darf man übrigens auch die ganze Klasse als readonly markieren, dann gilt es automatisch für alle Eigenschaften.

Du hast ein PHP-Projekt, in dem Daten unterwegs unerwartet ihren Wert ändern? Das ist ein typischer Fall dafür. Melde dich, ich schaue es mir an.

Quellen

#PHP 8#readonly#Constructor Promotion#DTO#Klassen

Du brauchst mehr als ein Snippet?

Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.

Projekt anfragen →