skip to content

In PHP 8.1 and later, where may a new expression be used as an initializer, and what did PHP 8.4 change about chaining on new?

level: middleimportance: should knowfreq 30%

answer

  1. objects as parameter defaults since 8.1
  2. also static variables, constants, attributes
  3. not property defaults or class constants
  4. a fresh object per call using the default
  5. 8.4: new Foo()->m() needs no wrapping

basics

~10 s

Since PHP 8.1, new may appear in parameter defaults, static variable initializers, global constants and attribute arguments, but not in property defaults or class constants. PHP 8.4 lets new Foo()->m() chain without wrapping parentheses.

solid answer

~40 s

PHP 8.1 ("new in initializers") allows `new` in a few constant-expression positions: **parameter defaults** such as `__construct(private Clock $clock = new SystemClock())`, **static variable** initializers, **global constants** (`const` and `define()`), and **attribute arguments**. It is still forbidden in **property defaults** and **class constants** (`New expressions are not supported in this context`), and the expression itself may not use a dynamic class name, an anonymous class, argument unpacking or non-constant arguments. A parameter default is evaluated each time a call uses it, so every such call gets a fresh object. Separately, PHP 8.4 made `new` with constructor parentheses directly dereferencable: `new Money(500, 'EUR')->add($fee)` works without wrapping the `new` in extra parentheses. The argument parentheses are required: `new Money->add()` is still a parse error.

code

php · 26 lines
php
<?php
declare(strict_types=1);

interface Clock { public function now(): DateTimeImmutable; }

final class SystemClock implements Clock
{
    public function now(): DateTimeImmutable { return new DateTimeImmutable(); }
}

final class TransferService
{
    public function __construct(
        private Clock $clock = new SystemClock(), // allowed since 8.1
    ) {}

    public function stamp(): string
    {
        return $this->clock->now()->format(DATE_ATOM);
    }
}

// PHP 8.4+: no wrapping parentheses needed
echo new TransferService()->stamp(), PHP_EOL;

// Still a parse error in 8.5: new TransferService->stamp();

go deeper

for a junior

Recall that since PHP 8.1 a parameter default can be an object created with new, such as a default clock.

for a middle

List where new is and is not allowed as an initializer, the restrictions on its arguments, and when a parameter default is evaluated.

for a senior

Use defaulted collaborators for testability without nullable parameters, and avoid defaults with side effects created on every call.

for a principal

Weigh defaulted collaborators against explicit wiring in a container, and plan syntax adoption such as 8.4 chaining against the minimum supported PHP version.

## New in initializers (PHP 8.1) An **initializer** is an expression PHP evaluates without running normal code first: a parameter's default value, a `static` variable's initial value, a constant's value, an attribute's arguments. Before PHP 8.1 these could hold only constant expressions built from scalars, arrays and constants. PHP 8.1 added `new` to a specific set of them. | Position | `new` allowed? | |---|---| | function and method parameter defaults (including promoted constructor parameters) | yes | | `static $x = new Foo;` static variable initializers | yes | | global `const C = new Foo;` and `define('C', new Foo)` | yes | | attribute arguments, `#[Assert(new Rule())]` | yes | | property defaults, `public Foo $x = new Foo;` | no | | static property defaults | no | | class constants, `const X = new Foo;` inside a class | no | The forbidden positions fail at compile time with `New expressions are not supported in this context`. The reason is evaluation timing: a property default would have to be evaluated per object, including for objects created without a constructor, and a class constant's side effects would run at an ill-defined moment. The `new` expression itself is also restricted. It may not use: - a **dynamic or non-string class name** (`new ($name)()`), - an **anonymous class** (`new class {}`), - **argument unpacking** (`new Foo(...$args)`), - arguments that are not themselves constant expressions (`new Foo($variable)`). Nested `new` and named arguments are fine: `new Retry(new Backoff(base: 100))`. ## The main use: defaulted dependencies The typical use is an optional collaborator with a sensible default, often combined with promotion: ```php public function __construct( private LedgerStore $store, private Clock $clock = new SystemClock(), ) {} ``` Before 8.1 this needed a nullable parameter and a line in the body (`$this->clock = $clock ?? new SystemClock();`), which also made the property nullable-looking to readers. Tests pass a fixed clock; production code passes nothing. A parameter default is evaluated **on each call that uses it**, so two `new Transfer(...)` calls without a clock argument receive two different `SystemClock` instances. A `static` variable initializer or a global constant is evaluated once. ## Chaining on `new` (PHP 8.4) Before PHP 8.4, calling a method on a freshly created object required wrapping the whole `new` expression: ```php $total = (new Money(500, 'EUR'))->add($fee); ``` Since **PHP 8.4**, a `new` expression **with constructor argument parentheses** can be dereferenced directly: - method call: `new Money(500, 'EUR')->add($fee)` - property read: `new Config()->timeout` - class constant and static member: `new Foo()::VERSION`, `new Foo()::create()` - array access on an `ArrayAccess` object: `new Registry()['key']` The parentheses after the class name are mandatory for this form. `new Money->add($fee)` is a parse error (`unexpected token "->"`); without argument parentheses the parser cannot tell it apart from `new $obj->prop`, which instantiates the class named by a property. You also cannot assign to or `unset()` a `new` expression. ## Practical guidance 1. Use `new` in parameter defaults for cheap, side-effect-free defaults (a null logger, a system clock). Avoid it for objects that open connections or files, because every defaulted call creates one. 2. For property defaults, keep assigning in the constructor or use promotion with a defaulted parameter. 3. Adopt the 8.4 chaining form only when the whole team targets 8.4 or later; the parser in older versions rejects it. ## Common mistakes - Writing the default on a property instead of a promoted parameter: rejected at compile time. - Passing a runtime value into a default, such as `new Rate($base)`: not a constant expression, so it fails to compile. - Expecting a shared instance: each defaulted call builds its own object, so a default that holds state (a cache, a counter) is not shared between instances. - Using the 8.4 chain in a library that still supports PHP 8.3: the file fails to parse on older runtimes, before any version check in code can run. ## Summary PHP 8.1 lets `new` appear in parameter defaults, static variable initializers, global constants and attribute arguments, with constant arguments only; property defaults and class constants remain off-limits. PHP 8.4 removed the extra parentheses around `new Foo(...)` before `->`, `::` or `[`.

  • Why can't a property default be `new Foo()` when a parameter default can?
    A property default would have to be evaluated for every object, including objects created without a constructor (reflection, deserialization), and there is no clean point in object creation to run it. PHP 8.1 therefore rejects it with `New expressions are not supported in this context`. Use a promoted constructor parameter with a `new` default instead.
  • Is `new Foo->bar()` valid in PHP 8.5?
    No. PHP 8.4's change applies only when the class name is followed by argument parentheses, as in `new Foo()->bar()`. Without them the parser reports `unexpected token "->"`, so either add `()` or keep the old `(new Foo)->bar()` form.

saying these in an interview costs you the question

  • Since PHP 8.1 a property can default to new Foo().
  • A new default parameter is created once and shared across calls.
  • new in initializers accepts any arguments, including variables.
  • PHP 8.4 allows new Foo->bar() without any parentheses.
  • Class constants can hold objects created with new since PHP 8.1.