Warum das für Symfony-Projekte alles ändert
Am 19. Februar 2026 reichte Nicolas Grekas vom Symfony Core Team einen RFC ein, der auf den ersten Blick nach einem kleinen syntaktischen Detail aussieht. Tatsächlich löst er ein Problem, das jeden von uns betrifft, der mit modernem PHP arbeitet: Readonly Properties und Constructor Property Promotion (CPP) passen nicht zusammen, sobald man Werte im Konstruktor validieren oder normalisieren will.
Der RFC „Allow Reassignment of Promoted Readonly Properties in Constructor" erlaubt genau eine Neuzuweisung von promoted readonly Properties innerhalb des Konstruktors. Das klingt technisch. Aber wer Value Objects, DTOs oder Domain Entities mit PHP 8.1+ schreibt, kennt das Problem: Man muss sich zwischen kompakter Syntax und sauberer Validierung entscheiden. Dieser RFC beseitigt diese Wahl.
In diesem Artikel schaue ich mir an, warum diese Änderung direkt aus der Symfony-Praxis kommt, welche Architektur-Implikationen sie hat und wie sie die Art verändert, wie wir Domain-Modelle in PHP designen. Keine theoretische Diskussion – sondern eine Einordnung aus der Perspektive von jemandem, der täglich mit genau diesen Patterns arbeitet.
Das Problem: Readonly trifft auf Constructor Property Promotion
Seit PHP 8.0 haben wir Constructor Property Promotion. Seit PHP 8.1 haben wir readonly Properties. Beide Features sind einzeln fantastisch. Zusammen erzeugen sie einen Zielkonflikt, den ich in jedem größeren Symfony-Projekt sehe.
Ein typisches Beispiel aus der Praxis: Ein DTO für API-Responses, das E-Mail-Adressen normalisiert speichern soll. Heute sieht das so aus:
class UserDto
{
public readonly string $email;
public function __construct(string $email)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Ungültige E-Mail');
}
$this->email = strtolower(trim($email));
}
}Das funktioniert. Aber es ist verbose. Die Property-Deklaration steht oben, die Parameter-Deklaration im Konstruktor – redundant. CPP würde das auf drei Zeilen reduzieren:
class UserDto
{
public function __construct(
public readonly string $email,
) {
// Hier will ich normalisieren – aber readonly verbietet es
$this->email = strtolower(trim($email)); // ERROR
}
}Das scheitert. Readonly Properties können nach der Initialisierung nicht mehr geändert werden. CPP initialisiert sie implizit, bevor der Konstruktor-Body läuft. Sobald mein Code ausgeführt wird, ist die Property bereits readonly.
Die aktuellen Workarounds sind alle unbefriedigend. Man kann CPP aufgeben und verbose bleiben. Man kann Validierung in statische Factory-Methoden auslagern. Oder man nutzt Property Hooks (PHP 8.4), die aber andere Semantik haben und bei jeder Zuweisung laufen, nicht nur im Konstruktor.
Aus meiner Erfahrung mit Symfony-Projekten: In 70% der DTOs und Value Objects brauche ich genau diese eine Normalisierung im Konstruktor. Telefonnummern formatieren, Strings trimmen, Enums aus Strings erstellen, Zeitstempel normalisieren. Das sind keine Edge-Cases – das ist der Alltag.
Die Lösung: Eine Neuzuweisung im Konstruktor erlauben
Der RFC ist chirurgisch präzise. Er erlaubt genau eine Neuzuweisung von promoted readonly Properties, solange der Konstruktor der Klasse auf dem Call Stack ist, die die Property per CPP deklariert hat.
Die Regeln:
- Die Property muss readonly sein
- Die Property muss per CPP promoted sein
- Nur eine Neuzuweisung ist erlaubt
- Nur solange der Konstruktor der CPP-deklarierenden Klasse läuft
- Methoden, die vom Konstruktor aufgerufen werden, dürfen zuweisen
- Asymmetric Visibility (
private(set),protected(set)) wird respektiert
Mit dem RFC wird das UserDto-Beispiel von oben möglich:
class UserDto
{
public function __construct(
public readonly string $email,
) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Ungültige E-Mail');
}
$this->email = strtolower(trim($email)); // OK!
}
}Kompakt, typsicher, validiert. Genau so, wie man Domain-Modelle schreiben will.
Was die Dokumentation nicht explizit erwähnt: Dieses Pattern ist direkt aus Symfony-Entities und API-Platform-Ressourcen gewachsen. Nicolas Grekas arbeitet seit Jahren an Symfony Core. Der RFC adressiert ein Problem, das in jedem größeren Symfony-Projekt dutzende Male auftaucht.
CPP-Ownership: Wer darf zuweisen?
Der RFC führt das Konzept der „CPP-owning class" ein. Das ist die Klasse, in deren Konstruktor die Property per CPP deklariert wurde. Nur deren Konstruktor-Chain darf die eine Neuzuweisung machen.
Beispiel mit Vererbung:
class Parent_
{
public function __construct(
public readonly string $prop = 'default',
) {
$this->prop = 'normalized'; // OK – Parent ist CPP-Owner
}
}
class Child extends Parent_
{
public function __construct()
{
parent::__construct();
$this->prop = 'child value'; // ERROR – Parent ist CPP-Owner
}
}Das ist konsequent. Die Property gehört semantisch zur Parent-Klasse. Child kann sie lesen, aber nicht neu initialisieren. Wenn Child die Property überschreiben will, muss es sie selbst per CPP re-deklarieren – was dann aber zu einem anderen Problem führt:
class Child extends Parent_
{
public function __construct(
public readonly string $prop = 'child default',
) {
parent::__construct(); // ERROR – prop ist schon initialisiert!
}
}Child's CPP läuft zuerst. Parent's CPP findet eine bereits initialisierte Property. Das schlägt fehl. Das ist kein Bug – das ist konsistente Semantik. Wenn beide Klassen die gleiche Property per CPP promoten, gibt es einen Konflikt.
Aus der Praxis: Das ist selten ein Problem. DTOs und Value Objects werden typischerweise nicht vererbt. Entities in Doctrine nutzen CPP selten für Vererbungshierarchien. Wo es relevant wird, ist bei abstrakten Base-Klassen – aber die nutzen CPP ohnehin nicht für readonly Properties.
Architektur-Implikationen: Was sich in Symfony-Projekten ändert
Diese RFC-Änderung ist kein syntaktischer Zucker. Sie verändert fundamentale Design-Patterns in PHP-Projekten. Ich sehe drei Bereiche, in denen sich Architektur-Entscheidungen verschieben:
1. DTOs und API-Responses werden kompakter
In Symfony API-Projekten schreibe ich dutzende DTOs. Mit API Platform oft hunderte. Jeder braucht Validierung und Normalisierung. Bisher war die Wahl: Entweder verbose Deklarationen oder auf Normalisierung im Konstruktor verzichten.
Mit diesem RFC wird ein typisches API-Response-DTO von 25 Zeilen auf 12 schrumpfen – bei gleichbleibender Validierung. Das klingt nach Kosmetik, bis man das 50. DTO schreibt.
Konkret für API Platform: Ressourcen mit #[ApiResource] können jetzt CPP nutzen und trotzdem Input-Normalisierung im Konstruktor machen. Das war bisher ein Entweder-Oder.
2. Value Objects verlieren Factory-Boilerplate
Value Objects in Domain-Driven Design nutzen oft Named Constructors für Validierung:
class Email
{
private function __construct(
public readonly string $value,
) {}
public static function fromString(string $email): self
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException();
}
return new self(strtolower(trim($email)));
}
}Mit dem RFC wird der private Konstruktor unnötig:
class Email
{
public function __construct(
public readonly string $value,
) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException();
}
$this->value = strtolower(trim($value));
}
}Gleiche Validierung, weniger Code. Für Value Object Libraries wie „moneyphp/money" oder eigene Domain-Model-Pakete ist das ein Game Changer.
3. Doctrine Entities bleiben unverändert – aus gutem Grund
Doctrine Entities nutzen CPP selten für readonly Properties. Der Grund: Doctrine hydratisiert Objekte über Reflection, ohne Konstruktoren aufzurufen. Readonly Properties würden Hydration blockieren.
Dieser RFC ändert daran nichts. Aber er macht einen hybriden Ansatz möglich: Value Objects in Entities können jetzt CPP + readonly nutzen, während die Entity selbst weiterhin mutable Properties hat.
class User
{
#[ORM\Embedded]
private Email $email; // Mutable für Doctrine
public function changeEmail(string $newEmail): void
{
$this->email = new Email($newEmail); // Email ist readonly intern
}
}Das war vorher komplizierter. Jetzt ist es idiomatisch.
Praxis-Perspektive: Wann nutzt man das wirklich?
Nach zehn Jahren PHP-Entwicklung habe ich gelernt: Nicht jedes neue Feature muss überall genutzt werden. Die Frage ist: Wann macht dieser RFC tatsächlich Code besser?
Einfache Ein-Schritt-Transformationen: Klarer Gewinn
Wenn die Normalisierung eine Zeile ist, ist CPP + Reassignment perfekt:
public function __construct(
public readonly string $name,
) {
$this->name = trim($name);
}Kurz, lesbar, typsicher. Das ersetzt 90% der Named-Constructor-Patterns in Value Objects.
Komplexe Multi-Step-Initialisierung: Traditionelle Deklaration bleibt besser
Wenn die Initialisierung mehrere Schritte hat oder Parameter-Typ und Property-Typ unterschiedlich sind, ist traditionelle Deklaration klarer:
class Config
{
public readonly string $cacheDir; // Non-nullable
public function __construct(
?string $cacheDir = null, // Nullable Parameter
) {
$validated = $cacheDir ?? sys_get_temp_dir();
if (!is_writable($validated)) {
throw new \RuntimeException('Cache directory not writable');
}
$this->cacheDir = realpath($validated);
}
}Hier wäre CPP + Reassignment verwirrend. Der Parameter ist nullable, die Property nicht. Die traditionelle Trennung macht das explizit.
Aus meiner Projekterfahrung: In etwa 70% der Cases ist CPP + Reassignment die bessere Wahl. In 30% ist traditionelle Deklaration klarer. Das RFC gibt mir die Option – es erzwingt nichts.
PHPDoc und Static Analysis: Wo Limits liegen
Ein Punkt, den der RFC nicht adressiert: Detaillierte Type-Annotations in PHPDoc.
class User
{
/** @var non-empty-string & lowercase-string */
public readonly string $email;
public function __construct(string $email)
{
// Validierung hier
}
}Mit CPP + Reassignment wird das schwieriger. PHPDoc-Annotations auf promoted Properties sind syntaktisch möglich, aber weniger prominent. Für Projekte mit starkem Static-Analysis-Setup (Psalm Level 1, PHPStan Level 8) kann traditionelle Deklaration weiterhin Vorteile haben.
Das ist kein Argument gegen den RFC – sondern eine Erinnerung, dass Code-Design immer Kontext braucht.
Vergleich mit Property Hooks: Unterschiedliche Semantik
PHP 8.4 hat Property Hooks eingeführt. Die können auch Normalisierung machen:
class Point
{
public function __construct(
public float $x { set => abs($value); },
public float $y { set => abs($value); },
) {}
}Das sieht ähnlich aus wie CPP + Reassignment. Aber die Semantik ist fundamental anders:
- Property Hooks laufen bei jeder Zuweisung – nicht nur im Konstruktor
- Property Hooks sind nicht readonly – sie können weiterhin überschrieben werden
- Property Hooks haben Runtime-Overhead – sie sind Funktionsaufrufe
CPP + Reassignment garantiert: Genau eine Normalisierung, nur bei Konstruktion, danach immutable. Property Hooks sind für andere Use Cases – z.B. virtuelle Properties oder Lazy-Loading.
Aus der Praxis: Für DTOs und Value Objects will ich readonly. Für Domain Events will ich immutability. Property Hooks sind dafür die falsche Lösung – dieser RFC ist die richtige.
Migration-Strategy: Wie nutzt man das in bestehenden Projekten?
Dieser RFC kommt frühestens in PHP 8.6 (Sommer 2027). Bis dahin ist die Frage: Wie bereite ich Projekte vor?
Schritt 1: CPP konsequent nutzen – ohne Reassignment
Nutzt CPP überall, wo keine Konstruktor-Logik nötig ist. Das funktioniert schon heute:
class ProductDto
{
public function __construct(
public readonly string $sku,
public readonly int $quantity,
public readonly \DateTimeImmutable $createdAt,
) {}
}Kein Boilerplate, volle Typsicherheit. Das ist bereits in PHP 8.1+ Best Practice.
Schritt 2: Traditionelle Deklaration, wo Validierung nötig ist
Für Properties mit Validierung bleibt vorerst die traditionelle Form:
class ProductDto
{
public readonly string $sku;
public function __construct(string $sku)
{
$this->sku = strtoupper(trim($sku));
}
}Sobald PHP 8.6 verfügbar ist, kann das zu CPP + Reassignment migriert werden. Der Unterschied: 6 Zeilen statt 10.
Schritt 3: Named Constructors dort behalten, wo sie Semantik hinzufügen
Nicht jeder Named Constructor sollte verschwinden. Wenn Email::fromString() vs. new Email() semantischen Mehrwert hat – z.B. unterschiedliche Validierungsregeln – dann behalten.
Aber wenn der Named Constructor nur existiert, weil CPP + readonly inkompatibel waren, dann ist er nach PHP 8.6 technische Schuld.
Einordnung: Warum dieser RFC wichtig ist
PHP bewegt sich seit Jahren Richtung Typsicherheit und Immutability. Readonly Properties waren ein großer Schritt. CPP hat Boilerplate eliminiert. Aber die Lücke dazwischen war spürbar.
Dieser RFC schließt diese Lücke nicht durch ein komplexes neues Feature, sondern durch eine chirurgisch präzise Lockerung einer bestehenden Regel. Das ist gutes Language Design: Komplexität reduzieren, nicht erhöhen.
Aus Sicht eines Symfony-Entwicklers: Das wird die Art verändern, wie wir DTOs, Value Objects und API-Ressourcen schreiben. Nicht radikal – aber spürbar. Weniger Boilerplate, mehr Fokus auf Geschäftslogik.
Und es kommt direkt aus der Praxis. Nicolas Grekas hat jahrelang Symfony-Code geschrieben und dieses Problem hunderte Male gesehen. Der RFC ist keine akademische Übung – er löst ein reales Problem.
Die Integration in bestehende Architekturen erfordert individuelle Analyse – besonders bei größeren Codebases mit starkem Static-Analysis-Setup. Lassen Sie uns das gemeinsam evaluieren.
Ausblick: Was kommt als nächstes?
Der RFC ist seit 19. Februar 2026 in Diskussion. Stand 12. März 2026 läuft die Voting-Phase an. Die Chancen stehen gut – die Community-Response war überwiegend positiv.
Parallel laufen zwei weitere RFCs:
- Readonly Variables (Joshua Rüsweg, 23. Feb 2026): Lokale Variablen readonly machen. Stößt auf Skepsis – der Nutzen ist unklar.
- „php-community" Fork (Daniil Gentili, 14. März 2026): Schnellerer Feature-Cycle außerhalb des Core-Release-Plans. Kontrovers – spaltet die Community.
Der Grekas-RFC ist der konservativste und praxisnahste der drei. Ich erwarte Akzeptanz für PHP 8.6.
Was das für Symfony bedeutet: Ab Symfony 8.2 oder 9.0 werden Best-Practice-Patterns aktualisiert. DTOs in Symfony Docs werden CPP + Reassignment zeigen. Das wird der neue Standard.
Für Shopware: Shopware 6.8 (2027) wird auf PHP 8.6 basieren können. Das bedeutet: Plugin-Entwicklung mit kompakteren DTOs, API-Extensions mit weniger Boilerplate. Ein kleiner, aber spürbarer Produktivitätsgewinn.
Zusammenfassung: Drei Key Takeaways
1. Der RFC löst ein reales Problem: Readonly + CPP sind heute inkompatibel mit Konstruktor-Validierung. Das betrifft jeden, der moderne PHP schreibt.
2. Die Lösung ist präzise: Genau eine Neuzuweisung, nur im Konstruktor, nur für promoted readonly Properties. Keine neue Komplexität – sondern eine gezielte Lockerung.
3. Die Auswirkung ist spürbar: DTOs, Value Objects, API-Ressourcen werden kompakter. Weniger Boilerplate, mehr Fokus auf Geschäftslogik. Das ist kein syntaktischer Zucker – das ist besseres Code-Design.
Dieser RFC kommt direkt aus der Symfony-Praxis. Er adressiert ein Problem, das Nicolas Grekas hunderte Male gesehen hat. Und genau deshalb wird er PHP nachhaltig verbessern.
Professionelle Beratung: Readonly-Patterns in Ihrem Projekt evaluieren
Die Umstellung auf readonly Properties und CPP erfordert strategische Entscheidungen – besonders in gewachsenen Codebases. Welche DTOs migrieren? Wo bleiben Named Constructors? Wie reagiert Ihr Static-Analysis-Setup?
Als Symfony-Entwickler mit Fokus auf API-Architekturen und Domain-Driven Design unterstütze ich Sie bei der Evaluierung: Welche Patterns passen zu Ihrer Infrastruktur? Wo lohnt sich Migration? Welche Refactorings haben Priorität?
Lassen Sie uns in einem Erstgespräch Ihre Code-Architektur analysieren. Ich zeige Ihnen konkret, wo readonly + CPP Mehrwert bringt – und wo traditionelle Deklaration besser bleibt.
Jetzt Erstgespräch vereinbaren
Über den Autor: Dennis Schwenker-Sanders ist PHP & Symfony-Entwickler mit Fokus auf API-Architekturen, Domain-Driven Design und moderne PHP-Patterns. Er unterstützt kleine Agenturen und KMU-Technikteams bei der Evaluierung und Implementierung von Symfony-basierten Backend-Lösungen. Mehr über Dennis