new in Initializers: Objekte als Default-Werte in PHP 8.1
AI generated
8.4
PHP · new in Initializers · PHP 8.1
new in Initializers
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.

9 Min. Lesezeit Sprachfeature PHP 8.1+

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.

11. FAQ: new in Initializers

1Was erlaubt new in Initializers konkret?
Seit PHP 8.1 darf new direkt als Default-Wert eines Parameters, einer Property oder als Attribut-Argument verwendet werden, statt nur konstanter Literale.
2Warum war das vor PHP 8.1 nicht möglich?
Default-Werte mussten zur Kompilierzeit als konstante Expression auswertbar sein, und eine Objektkonstruktion galt als Laufzeitvorgang mit möglichen Seiteneffekten.
3Wird bei jedem Aufruf dieselbe Objektinstanz wiederverwendet?
Nein, bei jedem Aufruf ohne explizit übergebenes Argument wird eine neue, unabhängige Instanz erzeugt, genau wie bei manueller Konstruktion im Methodenkörper.
4Kann man beliebige Ausdrücke als Argument im new-Initializer verwenden?
Nein, die Argumente müssen selbst konstante Expressions sein, ein Funktionsaufruf zur Laufzeit wie getenv() ist nicht erlaubt.
5Funktioniert new in Initializers auch bei Attributen?
Ja, ein Attribut kann ein konfiguriertes Objekt als Argument entgegennehmen, statt nur primitive Werte zu akzeptieren.
6Wird der Parameter dadurch automatisch nullable?
Nein, im Gegenteil, der Parameter kann nicht-nullable bleiben, weil der Default-Wert immer eine echte Instanz und niemals null ist.
7Kann man Enum-Cases als Argument im new-Initializer nutzen?
Ja, Enum-Cases sind konstante Expressions und lassen sich problemlos als Argument innerhalb eines new-Aufrufs im Initializer verwenden.
8Gibt es Warnungen bei privaten Klassen als Default in public Methoden?
Statische Analyse-Werkzeuge wie PHPStan markieren das häufig, weil die Signatur damit ein eigentlich internes Implementierungsdetail preisgibt.
9Ersetzt new in Initializers Dependency Injection Container?
Nein, es ergänzt sie, produktiver Code nutzt weiterhin einen Container, während Tests und Skripte von sinnvollen Defaults direkt in der Signatur profitieren.
10Funktioniert das auch bei Constructor Property Promotion?
Ja, die Kombination ist besonders praktisch, weil Default-Wert und Property-Deklaration bereits in einer einzigen Zeile zusammenfallen.