new in Initializers — Real Default Objects
Default parameter values had to be constant expressions, so you could default to a scalar, an array, or null, but never an object. The result was the null-default dance, in every constructor, forever.
// Before 8.1: null means "I'll make one myself, later".
class Service {
private LoggerInterface $logger;
public function __construct(?LoggerInterface $logger = null) {
$this->logger = $logger ?? new NullLogger();
}
}
The type says the logger might be null. It never is. Every reader has to check the body to learn that, and the nullable type infects any code that touches the property.
// 8.1: the default is the object, and the type stops lying.
class Service {
public function __construct(
private LoggerInterface $logger = new NullLogger(),
) {}
}
It composes with the rest of 8.0 and 8.1 exactly as you would hope: constructor promotion, named arguments, and attribute arguments all take new now.
class Service {
public function __construct(
private LoggerInterface $logger = new NullLogger(),
private Clock $clock = new SystemClock(),
) {}
}
new Service(clock: new FrozenClock('2026-07-15')); // override just the one
#[Route('/users', validator: new StrictValidator())] // works in attributes too
function users() {}
The object is constructed per call, not once and shared, so there is no accidental global state hiding in a default. And it is still restricted where it must be: no new in property defaults, constants, or static variables, because those are evaluated once at a point where running arbitrary constructors would be a bad idea.
Significance: Types That Mean It
?Foo $foo = null and patched it in the body. That single workaround produced thousands of nullable types that were never actually null, and every one of them cost a null check downstream. Removing it is why 8.1 constructors read like declarations instead of setup scripts.