Objekte als Default-Werte in PHP 8.1
Bis PHP 8.0 musste jeder Default-Wert eine konstante Expression sein, eine neue Objektinstanz als Default war schlicht nicht erlaubt. Seit PHP 8.1 darf new direkt in Initializern stehen, in Parametern, Attribut-Argumenten und Property-Deklarationen, und macht damit einen ganzen Katalog von Umwegen überflüssig.
Inhaltsverzeichnis
- 1. Die alte Einschränkung: Default-Werte mussten konstant sein
- 2. Was sich mit PHP 8.1 ändert
- 3. Kombination mit Constructor Property Promotion
- 4. Objektinstanzen als Attribut-Argumente
- 5. Vermeidung des Null-Object-Umwegs
- 6. Einschränkungen: welche Ausdrücke erlaubt sind
- 7. Enum-Cases und Konstanten im new-Initializer kombinieren
- 8. Sichtbarkeit: private Klassen als Default in öffentlichen Signaturen
- 9. Praxisbeispiel: Dependency Injection ohne Konstruktor-Body
- 10. Zusammenfassung
- 11. FAQ
1. Die alte Einschränkung: Default-Werte mussten konstant sein
Vor PHP 8.1 durften Default-Werte in Parameterlisten ausschließlich konstante Expressions sein: Literale, Klassenkonstanten oder Kombinationen davon, aber niemals ein Aufruf von new. Der Grund lag darin, dass Default-Werte zur Kompilierzeit ausgewertet werden mussten, und eine Objektkonstruktion ist grundsätzlich ein Laufzeitvorgang mit potenziellen Seiteneffekten.
Wer trotzdem ein Objekt als sinnvollen Default für einen Parameter wollte, etwa einen leeren Logger oder eine Standardkonfiguration, musste auf null als Platzhalter ausweichen und im Methodenkörper explizit prüfen, ob der Parameter null ist, um dann selbst eine Instanz zu erzeugen. Dieser Umweg war Boilerplate, die sich in jeder betroffenen Methode wiederholte.
Diese Einschränkung betraf nicht nur Konstruktoren, sondern jede Funktion und Methode mit optionalen Parametern gleichermaßen, unabhängig davon, ob es sich um eine öffentliche API-Methode oder eine private Hilfsfunktion handelte. Für Bibliotheken mit vielen konfigurierbaren Kollaborateuren summierte sich dieser Boilerplate über Dutzende Klassen hinweg zu spürbarem Wartungsaufwand.
2. Was sich mit PHP 8.1 ändert
Mit PHP 8.1 wird new selbst zu einer erlaubten Expression innerhalb eines Initializers, solange die Argumente des Konstruktors ihrerseits wieder konstante Expressions sind. Das erlaubt new NullLogger() oder new DateInterval('P0D') direkt als Default-Wert eines Parameters, ohne den Umweg über null.
Technisch handelt es sich dabei nicht um eine einmalig zur Kompilierzeit erzeugte, geteilte Instanz, sondern um eine neue Objektkonstruktion bei jedem einzelnen Aufruf ohne explizit übergebenes Argument. Jeder Aufruf ohne Argument erhält also eine frische, unabhängige Instanz, genau wie bei einer manuellen Konstruktion im Methodenkörper.
final class RequestHandler
{
public function __construct(
private LoggerInterface $logger = new NullLogger(),
) {
}
}
// A fresh NullLogger instance is created for every call without an explicit argument
$handler = new RequestHandler();
3. Kombination mit Constructor Property Promotion
Besonders praktisch wird new in Initializers in Kombination mit Constructor Property Promotion, wo Default-Wert und Property-Deklaration ohnehin in einer einzigen Zeile zusammenfallen. Statt einer eigenen Zeile im Konstruktorkörper für die Fallback-Logik reicht die Default-Wert-Angabe direkt im Parameter.
Das reduziert den Konstruktorkörper in vielen Fällen auf null Zeilen zusätzlichen Code, weil sowohl die Zuweisung als auch die Fallback-Erzeugung vollständig in der Signatur ausgedrückt sind. Für Klassen mit mehreren optionalen Kollaborateuren summiert sich diese Ersparnis deutlich.
final class OrderProcessor
{
public function __construct(
private ClockInterface $clock = new SystemClock(),
private CacheInterface $cache = new ArrayCache(),
) {
}
}
4. Objektinstanzen als Attribut-Argumente
new in Initializers funktioniert nicht nur bei Parametern und Properties, sondern auch als Argument innerhalb von Attributen. Ein Attribut kann damit direkt ein konfiguriertes Objekt statt nur primitiver Werte entgegennehmen, etwa eine vorkonfigurierte Cache-Policy oder eine strukturierte Validierungsregel.
Das erweitert den praktischen Nutzen von Attributen erheblich, weil Konfiguration nicht mehr zwangsläufig über verschachtelte Arrays oder Strings ausgedrückt werden muss, die dann erst zur Laufzeit wieder in ein Objekt umgewandelt werden. Stattdessen steht das fertige Objekt bereits in der Attribut-Deklaration selbst.
#[Attribute]
final class Cacheable
{
public function __construct(public CachePolicy $policy = new CachePolicy(ttl: 300))
{
}
}
final class ProductController
{
#[Cacheable(new CachePolicy(ttl: 3600))]
public function show(int $id): Response
{
// ...
}
}
5. Vermeidung des Null-Object-Umwegs
Der klassische Umweg vor PHP 8.1 sah in nahezu jeder betroffenen Methode identisch aus: Parameter mit Default null deklarieren, im Methodenkörper auf null prüfen und bei Bedarf selbst eine Instanz erzeugen. Dieses Muster musste an jeder einzelnen Stelle wiederholt werden, an der der Parameter verwendet wurde.
Mit new in Initializers verschwindet dieser Boilerplate vollständig aus dem Methodenkörper. Die Fallback-Logik steht ein einziges Mal in der Signatur, der restliche Code kann sich darauf verlassen, dass der Parameter niemals null ist, was zusätzlich die Nullable-Typisierung des Parameters selbst überflüssig macht.
// Before PHP 8.1: manual null check inside every method
public function handle(?LoggerInterface $logger = null): void
{
$logger = $logger ?? new NullLogger();
$logger->info('handled');
}
// PHP 8.1+: the fallback lives entirely in the signature
public function handle(LoggerInterface $logger = new NullLogger()): void
{
$logger->info('handled');
}
6. Einschränkungen: welche Ausdrücke erlaubt sind
Die Argumente des new-Aufrufs innerhalb eines Initializers müssen selbst wieder konstante Expressions sein: Literale, Klassenkonstanten, Enum-Cases oder verschachtelte new-Aufrufe mit ebenfalls konstanten Argumenten. Ein Funktionsaufruf wie new Logger(getenv('LOG_PATH')) ist dagegen nicht erlaubt, weil getenv zur Laufzeit ausgewertet wird und keine konstante Expression ist.
Diese Einschränkung ist bewusst gewählt: Ein Default-Wert soll weiterhin deterministisch und ohne externe Abhängigkeiten auswertbar sein, allein basierend auf dem Quellcode selbst. Wer echte Laufzeitwerte für den Default braucht, bleibt weiterhin auf den klassischen Null-Check im Methodenkörper angewiesen. Ein new-Aufruf, dessen Argumente eine Umgebungsvariable, einen Datenbankwert oder das Ergebnis einer beliebigen Funktion einbeziehen, bleibt also weiterhin ausgeschlossen, unabhängig davon, wie harmlos der Aufruf im Einzelfall erscheinen mag.
7. Enum-Cases und Konstanten im new-Initializer kombinieren
Da Enum-Cases selbst konstante Expressions sind, lassen sie sich problemlos als Argument innerhalb eines new-Initializers verwenden, etwa um ein Value Object mit einem festen Enum-Wert als Default zu konstruieren. Das verbindet zwei separate PHP-8.1-Features zu einem gemeinsamen, sehr lesbaren Muster.
Ebenso lassen sich Klassenkonstanten der aktuellen oder einer anderen Klasse als Argument nutzen, was Default-Werte konfigurierbar macht, ohne dass die Konfiguration außerhalb der Klassendefinition verstreut liegt. Eine Anwendung kann so eigene Default-Policies zentral als Konstanten pflegen und trotzdem direkt in Signaturen referenzieren.
Auch die Kombination mehrerer Konstanten innerhalb eines einzigen new-Aufrufs ist erlaubt, solange jede einzelne Konstante zur Kompilierzeit auflösbar ist. Ein Value Object kann so mit einer festen Kombination aus Enum-Case und numerischer Konstante als Default vorbelegt werden, ohne dass dafür eine separate Factory-Methode nötig wäre.
8. Sichtbarkeit: private Klassen als Default in öffentlichen Signaturen
Ein Detail, das leicht übersehen wird: Wird eine private oder protected Klasse als Default-Wert in einer public Methode verwendet, ist das zwar syntaktisch erlaubt, führt aber häufig zu einer Warnung statischer Analyse-Werkzeuge, weil die Signatur damit ein Detail preisgibt, das eigentlich implementierungsintern bleiben sollte.
Für öffentliche APIs empfiehlt es sich deshalb, den Default-Typ auf eine öffentlich sichtbare Klasse oder ein Interface zu beschränken, selbst wenn die konkrete Default-Implementierung intern bleiben könnte. PHPStan und Psalm markieren solche Sichtbarkeits-Inkonsistenzen zuverlässig.
Bei Bibliotheken, die für den externen Gebrauch veröffentlicht werden, ist diese Sorgfalt besonders wichtig, weil eine als Default sichtbare interne Klasse faktisch Teil der öffentlichen API wird, sobald ein Aufrufer sich auf ihre konkreten Methoden statt auf das Interface verlässt, selbst wenn das nie beabsichtigt war.
9. Praxisbeispiel: Dependency Injection ohne Konstruktor-Body
In der Praxis zeigt sich der größte Nutzen von new in Initializers bei Klassen mit mehreren optionalen Kollaborateuren, die in Produktionscode meist über einen Dependency-Injection-Container ersetzt werden, in Tests oder einfachen Skripten aber sinnvolle Defaults brauchen. Der Konstruktorkörper bleibt dabei vollständig leer.
Das Resultat ist eine Klasse, die in produktiver Umgebung vollständig über Dependency Injection konfiguriert wird, sich aber in einem einfachen Skript oder Test ohne jede zusätzliche Konfiguration instanziieren lässt, weil jeder optionale Kollaborateur bereits einen funktionierenden Default direkt in der Signatur mitbringt. Für Kommandozeilen-Tools und kleinere Wartungsskripte, die keinen vollständigen Container aufbauen wollen, ist das ein spürbarer Produktivitätsgewinn gegenüber der manuellen Verdrahtung jeder einzelnen Abhängigkeit.
final class ReportGenerator
{
public function __construct(
private ClockInterface $clock = new SystemClock(),
private LoggerInterface $logger = new NullLogger(),
private CacheInterface $cache = new ArrayCache(),
) {
}
}
// Works out of the box in a script or test, no container wiring needed
$report = new ReportGenerator();
| Situation | Vor PHP 8.1 | Seit PHP 8.1 mit new in Initializers |
|---|---|---|
| Objekt als Default-Wert | Nicht erlaubt, nur konstante Expressions | Erlaubt, sofern Konstruktor-Argumente konstant sind |
| Fallback-Logik | Manueller Null-Check im Methodenkörper | Vollständig in der Signatur ausgedrückt |
| Nullable-Typisierung | Parameter musste nullable sein | Parameter kann nicht-nullable bleiben |
| Attribut-Argumente | Nur primitive Werte und Konstanten | Konfigurierte Objektinstanzen direkt möglich |
| Laufzeitwerte als Default | Nicht möglich | Weiterhin nicht möglich, nur konstante Ausdrücke |
| Kombination mit Property Promotion | Erforderte separaten Konstruktorkörper | Eine einzige Zeile pro Parameter reicht |
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
new in Initializers
Kernidee
new darf seit PHP 8.1 direkt als Default-Wert stehen, sofern die Konstruktor-Argumente konstant sind.
Vorteil
Der Null-Object-Umweg mit manuellem Null-Check im Methodenkörper wird vollständig überflüssig.
Erweiterung
Auch Attribut-Argumente dürfen konfigurierte Objektinstanzen statt nur primitiver Werte enthalten.
Grenze
Die Argumente des new-Aufrufs müssen selbst konstante Expressions bleiben, keine Funktionsaufrufe zur Laufzeit.