wie Sichtbarkeit, Typ und readonly im Konstruktorkopf verschmelzen
Constructor Property Promotion reduziert die klassische Dreifach-Wiederholung aus Property-Deklaration, Konstruktor-Parameter und Zuweisung im Body auf eine einzige Zeile pro Feld, ohne dabei Typsicherheit, Sichtbarkeit oder readonly-Garantien zu opfern, und macht gerade Value Objects und DTOs spürbar kompakter und wartbarer.
Inhaltsverzeichnis
- 1. Das Boilerplate-Problem klassischer Konstruktoren
- 2. Syntax: Sichtbarkeit direkt im Konstruktor deklarieren
- 3. readonly-Properties mit Constructor Property Promotion kombinieren
- 4. Default-Werte, Nullable-Types und Union-Types promoten
- 5. Promotion mit Interfaces und typisierten Objekttypen
- 6. Attribute an promoted Properties anhängen
- 7. Was NICHT promotet werden kann
- 8. Promotion in Value Objects und unveränderlichen Datenstrukturen
- 9. Bestehende Klassen schrittweise zu Promotion migrieren
- 10. Zusammenfassung
- 11. FAQ
1. Das Boilerplate-Problem klassischer Konstruktoren
Vor PHP 8.0 musste jede einfache Eigenschaft einer Klasse dreimal geschrieben werden: einmal als Property-Deklaration mit Sichtbarkeit und Typ, einmal als Konstruktor-Parameter mit demselben Typ, und einmal als Zuweisung $this->property = $property; im Konstruktor-Body. Bei einer Klasse mit acht Feldern bedeutete das über zwanzig Zeilen reinen Wiederholungscode, bevor überhaupt Geschäftslogik beginnt. Constructor Property Promotion wurde genau für dieses Problem entwickelt.
Diese Wiederholung war nicht nur mühsam zu tippen, sie war auch eine eigene Fehlerquelle: Ändert sich der Typ einer Property, muss er an zwei Stellen synchron gehalten werden, an der Deklaration und am Parameter. Ein Refactoring-Tool erledigt das zuverlässig, ein manueller Edit vergisst gelegentlich eine der beiden Stellen, was zu einer stillen Diskrepanz zwischen deklariertem Property-Typ und tatsächlich akzeptiertem Parameter-Typ führen kann.
Seit PHP 8.0 löst Constructor Property Promotion dieses Problem, indem Sichtbarkeit, Typ und Parametername direkt im Konstruktorkopf zur Property-Deklaration werden. PHP erzeugt Property-Deklaration und Zuweisung automatisch aus dieser einen Stelle. Die Klasse verhält sich nach außen exakt wie vorher, nur der Quellcode ist erheblich kürzer und die Synchronität zwischen Typ und Property ist strukturell garantiert, nicht mehr durch Disziplin.
2. Syntax: Sichtbarkeit direkt im Konstruktor deklarieren
Die Syntax von Constructor Property Promotion fügt lediglich einen Sichtbarkeitsmodifizierer, public, protected oder private, vor einen Konstruktor-Parameter ein. Dieser Modifizierer ist das Signal an PHP, aus diesem Parameter automatisch eine Klasseneigenschaft zu erzeugen und den übergebenen Wert bei der Instanziierung zuzuweisen, ohne dass der Entwickler eine Zeile im Konstruktor-Body schreiben muss.
Parameter ohne Sichtbarkeitsmodifizierer bleiben gewöhnliche, nicht promotete Parameter, die nur innerhalb des Konstruktors existieren, sofern sie nicht manuell zugewiesen werden. Das erlaubt, promotete und nicht promotete Parameter im selben Konstruktor zu mischen, etwa wenn ein Parameter nur zur Validierung dient, aber selbst nicht als Property gespeichert werden soll.
<?php
declare(strict_types=1);
// Before promotion: property, parameter, and assignment repeated three times
final class ProductBefore
{
private string $sku;
private string $name;
private float $price;
public function __construct(string $sku, string $name, float $price)
{
$this->sku = $sku;
$this->name = $name;
$this->price = $price;
}
}
// With Constructor Property Promotion: one declaration per field
final class Product
{
public function __construct(
private string $sku,
private string $name,
private float $price,
) {
}
public function sku(): string
{
return $this->sku;
}
}
3. readonly-Properties mit Constructor Property Promotion kombinieren
Seit PHP 8.1 lässt sich das Schlüsselwort readonly direkt mit Constructor Property Promotion kombinieren: public readonly string $sku im Konstruktorkopf erzeugt eine Property, die nach der Initialisierung im Konstruktor niemals mehr verändert werden kann. Jeder weitere Schreibversuch außerhalb des deklarierenden Scopes wirft einen Error. Diese Kombination ist heute der Standardweg, unveränderliche Objekte in PHP zu bauen.
Der Vorteil gegenüber separat deklarierten readonly-Properties liegt wieder in der Kompaktheit: Statt readonly an der Property-Deklaration und den Typ am Parameter jeweils einzeln zu pflegen, steht beides zusammen in einer Zeile. Für eine Klasse, deren gesamter Zweck darin besteht, unveränderliche Daten zu transportieren, etwa ein Geldbetrag oder eine E-Mail-Adresse als Value Object, ist diese Kombination aus Constructor Property Promotion und readonly heute nahezu immer die richtige Wahl.
<?php
declare(strict_types=1);
// Immutable value object: promoted, readonly properties, validated once
final class Money
{
public function __construct(
public readonly int $amountInCents,
public readonly string $currency,
) {
if ($this->amountInCents < 0) {
throw new InvalidArgumentException('Amount must not be negative.');
}
}
public function add(self $other): self
{
if ($other->currency !== $this->currency) {
throw new InvalidArgumentException('Currency mismatch.');
}
// Returns a new instance instead of mutating the current one
return new self($this->amountInCents + $other->amountInCents, $this->currency);
}
}
$price = new Money(1999, 'EUR');
$shipping = new Money(495, 'EUR');
$total = $price->add($shipping);
4. Default-Werte, Nullable-Types und Union-Types promoten
Promotete Parameter unterstützen dieselben Default-Werte wie gewöhnliche Konstruktor-Parameter. public readonly ?string $note = null zeigt, wie sich Nullable-Types und ein Standardwert kombinieren lassen, ohne dass zusätzlicher Code im Body nötig wird. Der Standardwert wird exakt einmal an dieser Stelle gepflegt, nicht zusätzlich an einer separaten Property-Deklaration wiederholt.
Auch Union-Types funktionieren bei Constructor Property Promotion unverändert: public readonly int|string $identifier promotet einen Parameter, der wahlweise einen Integer oder einen String akzeptiert. Wichtig dabei: Der Default-Wert eines promoted Parameters muss weiterhin ein konstanter Ausdruck sein, etwa null, ein Literal oder eine Klassenkonstante, dieselbe Einschränkung, die für gewöhnliche Parameter-Defaults in PHP gilt.
<?php
declare(strict_types=1);
final class Comment
{
public function __construct(
public readonly string $body,
// Nullable with a default: promoted, no body assignment needed
public readonly ?string $note = null,
// Union type promoted just like on a classic parameter
public readonly int|string $authorId = 'anonymous',
// Default backed by a class constant, a constant expression
public readonly string $status = self::STATUS_PENDING,
) {
}
private const string STATUS_PENDING = 'pending';
}
$comment = new Comment(body: 'Great article!');
5. Promotion mit Interfaces und typisierten Objekttypen
Promotete Parameter sind nicht auf Skalartypen beschränkt. Ein Parameter kann als Interface, als konkrete Klasse oder sogar als typisiertes Array-Element deklariert werden. public readonly LoggerInterface $logger promotet eine injizierte Abhängigkeit direkt zu einer Property, ohne dass eine separate Zeile im Body nötig ist, was Constructor Property Promotion gerade bei Dependency Injection zu einem natürlichen Werkzeug macht.
Diese Kombination aus Promotion und Interface-Typisierung entkoppelt die Klasse von einer konkreten Implementierung, genau wie bei einem klassisch deklarierten Konstruktor-Parameter. Der einzige Unterschied ist die kompaktere Schreibweise: Statt Interface-Import, Property-Deklaration, Parameter und Zuweisung an vier verschiedenen Zeilen zu pflegen, steht die komplette Information in einer einzigen Parameterzeile im Konstruktorkopf.
6. Attribute an promoted Properties anhängen
Da eine promotete Property syntaktisch weiterhin eine reguläre Property ist, lassen sich PHP-Attribute genauso direkt davor platzieren wie bei einer klassisch deklarierten Property. #[Sensitive] public readonly string $iban zeigt, wie sich Constructor Property Promotion und Attribute kombinieren lassen, ohne dass die Kompaktheit der Promotion verloren geht. Die Reflection-API liest solche Attribute über ReflectionParameter::getAttributes() ebenso zuverlässig aus wie über eine klassische Property.
Diese Kombination ist besonders bei Validierungs- und Serialisierungssystemen verbreitet, die Attribute nutzen, um Regeln direkt an DTOs zu binden. Ein DTO mit acht promoted, jeweils attributierten Properties bleibt trotzdem übersichtlich, weil jede Zeile alle relevanten Informationen zu genau einem Feld auf einen Blick zeigt: Sichtbarkeit, Typ, Attribut und gegebenenfalls Default-Wert.
<?php
declare(strict_types=1);
// Attributes combine cleanly with promoted, readonly properties
final class CreateCustomerRequest
{
public function __construct(
#[NotBlank]
public readonly string $name,
#[NotBlank]
#[Sensitive('email address')]
public readonly string $email,
#[Length(min: 8, max: 128)]
public readonly string $password,
public readonly ?string $referralCode = null,
) {
}
}
7. Was NICHT promotet werden kann
Constructor Property Promotion ist an konkrete Grenzen gebunden. Statische Properties können niemals promotet werden, da Promotion konzeptionell an eine konkrete Instanziierung gebunden ist, statische Properties aber klassenweit und unabhängig von einer Instanz existieren. Ein Versuch, public static string $x in einem Konstruktorkopf zu schreiben, führt zu einem Parse-Fehler.
Ebenso wenig lassen sich berechnete Default-Werte promoten. Ein Ausdruck wie public readonly DateTimeImmutable $createdAt = new DateTimeImmutable() ist als Parameter-Default nicht erlaubt, denn PHP verlangt für Default-Werte konstante Ausdrücke, keine Funktionsaufrufe oder Objektinstanziierungen zur Aufrufzeit. Wer einen berechneten Default benötigt, muss den Parameter nullable machen und die Berechnung im Konstruktor-Body durchführen, was einen Teil der Kompaktheit von Constructor Property Promotion zunichtemacht.
Auch Properties mit komplexer, mehrzeiliger Initialisierungslogik, etwa Validierung mehrerer Felder gegeneinander, gehören weiterhin in den Konstruktor-Body, nicht in die Parameterzeile selbst. Promotion ersetzt reine Zuweisung, nicht Geschäftslogik. Enthält eine Property-Initialisierung mehr als eine simple Direktzuweisung, ist der Body weiterhin der richtige Ort dafür, unabhängig davon, ob der Parameter selbst promotet ist.
| Aufgabe | Klassisch (viel Code) | Mit Constructor Property Promotion | Vorteil |
|---|---|---|---|
| 3 Properties initialisieren | 3x Deklaration plus 3x Zuweisung im Body (6 Zeilen) | 3 Parameter im Konstruktorkopf (3 Zeilen) | Halbe Codemenge, weniger Fehlerquellen |
| Unveränderliches Value Object | private Property, manuell im Body zugewiesen | public readonly direkt promotet | Immutability vom Compiler erzwungen |
| Typsicherheit der Property | Property-Typ separat gepflegt, kann von Parameter-Typ abweichen | Ein Typ für Parameter und Property zugleich | Keine Divergenz zwischen Typen möglich |
| DTO mit 8 Feldern | Rund 24 Zeilen (Deklaration, Parameter, Zuweisung) | 8 Zeilen im Konstruktorkopf | Deutlich lesbarer, weniger Wartungsaufwand |
| Attribute pro Property | Zusätzliche Zeilen vor jeder Property-Deklaration | Attribute direkt vor dem promoted Parameter | Kompakter, gleiche Aussagekraft |
8. Promotion in Value Objects und unveränderlichen Datenstrukturen
Value Objects sind der Anwendungsfall, bei dem Constructor Property Promotion ihren größten Nutzen entfaltet. Ein Value Object hat per Definition keinen veränderlichen Zustand nach der Erzeugung, jede scheinbare Änderung liefert eine neue Instanz zurück. Diese Eigenschaft deckt sich exakt mit dem, was promotete readonly-Properties ohnehin erzwingen, wodurch Syntax und Konzept nahtlos zusammenpassen.
Bei DTOs, die Daten zwischen Schichten transportieren, etwa von einer HTTP-Anfrage zu einem Domänenobjekt, ist die Kombination ebenfalls verbreitet. Ein DTO mit zehn oder mehr Feldern bleibt durch Constructor Property Promotion als einzige, kompakte Parameterliste lesbar, während dieselbe Klasse ohne Promotion leicht dreißig oder vierzig Zeilen reinen Boilerplate benötigen würde, bevor die erste Zeile Geschäftslogik überhaupt beginnt.
9. Bestehende Klassen schrittweise zu Promotion migrieren
Eine Migration bestehender Klassen zu Constructor Property Promotion ist rückwärtskompatibel und kann Klasse für Klasse erfolgen, ohne dass Aufrufer etwas davon bemerken. Solange sich Sichtbarkeit, Typ und Parameterreihenfolge nicht ändern, bleibt die öffentliche Schnittstelle der Klasse identisch, nur die interne Implementierung wird kompakter. Für neue Klassen ist Promotion heute in aktuellem PHP-Code praktisch Standard.
Werkzeuge wie PHP-CS-Fixer oder Rector bieten automatisierte Regeln an, die klassische Konstruktoren maschinell zu promoteten Parametern umschreiben, inklusive korrekter Übernahme von Typen und Sichtbarkeiten. Für große, gewachsene Codebasen ist dieser automatisierte Weg deutlich sicherer als eine manuelle Migration, weil er das Risiko ausschließt, bei der Umstellung versehentlich Typ oder Sichtbarkeit einer Property zu verändern.
<?php
declare(strict_types=1);
// Before: classic constructor, candidate for automated migration
final class AddressBefore
{
private string $street;
private string $city;
private string $postalCode;
public function __construct(string $street, string $city, string $postalCode)
{
$this->street = $street;
$this->city = $city;
$this->postalCode = $postalCode;
}
}
// After: Rector's constructor promotion rule produces exactly this shape
final class Address
{
public function __construct(
private readonly string $street,
private readonly string $city,
private readonly string $postalCode,
) {
}
}
10. Zusammenfassung
Constructor Property Promotion ersetzt die klassische Dreifach-Wiederholung aus Property-Deklaration, Konstruktor-Parameter und Body-Zuweisung durch eine einzige Zeile pro Feld im Konstruktorkopf. Sichtbarkeitsmodifizierer, Typdeklarationen, readonly, Default-Werte und sogar PHP-Attribute lassen sich direkt an dieser Stelle kombinieren, ohne dass Typsicherheit oder Ausdruckskraft verloren gehen.
Die Grenzen liegen bei statischen Properties, die grundsätzlich nicht promotet werden können, und bei berechneten Default-Werten, die weiterhin im Konstruktor-Body initialisiert werden müssen. Für Value Objects, DTOs und Klassen mit injizierten Abhängigkeiten ist Constructor Property Promotion heute der Standardweg, gerade in Kombination mit readonly für echte Unveränderlichkeit.
Constructor Property Promotion - Das Wichtigste auf einen Blick
Syntax
Sichtbarkeitsmodifizierer direkt vor dem Konstruktor-Parameter erzeugt Property-Deklaration und Zuweisung automatisch.
readonly kombinieren
public readonly direkt im Konstruktorkopf ist der Standardweg für unveränderliche Value Objects seit PHP 8.1.
Grenzen
Keine statischen Properties, keine berechneten Default-Werte. Komplexe Initialisierung bleibt im Konstruktor-Body.
Migration
Rückwärtskompatibel, Klasse für Klasse möglich. Rector und PHP-CS-Fixer automatisieren die Umstellung sicher.
11. FAQ: Constructor Property Promotion
1Seit welcher Version gibt es Promotion?
2Promotete und nicht promotete Parameter mischen?
3Statische Property promoten?
4Berechneter Default-Wert möglich?
5Attribute an promoted Properties?
6Ist Promotion nur Syntax-Zucker?
7Union-Types bei promoted Parametern?
8Müssen alle Properties promotet werden?
9Große Codebasen sicher migrieren?
10Passt Promotion zu Value Objects?
Mironsoft
PHP-Architektur, Codequalität und Magento-Entwicklung
Bestehende Klassen zu Constructor Property Promotion migrieren?
Wir automatisieren die Migration bestehender Konstruktoren zu Constructor Property Promotion mit readonly, inklusive Rector-Regeln, Code-Review und vollständiger PHPStan-Absicherung für eure Codebasis.
Automatisierte Migration
Rector-Regeln für klassenweite Umstellung auf Promotion
Value-Object-Design
Unveränderliche DTOs und Value Objects mit readonly konzipieren
Code-Review
Prüfung bestehender Konstruktoren auf Migrationspotenzial