PHP 8.4: Chain Methods on new Without the Extra Parentheses

PHP 8.4 new without parentheses lets you chain straight off new, so (new Foo())->bar() becomes new Foo()->bar(). Learn the one rule and the precedence gotchas.

Steven Richardson
Steven Richardson
· 6 min read

If you have written any builder-heavy PHP, you know the dance: (new ReflectionClass($model))->getShortName(). That outer pair of parentheses around new has been mandatory since PHP 5.4 just to chain a method onto a fresh instance. PHP 8.4 finally kills it. With new without parentheses you write new ReflectionClass($model)->getShortName() and move on. One small rule comes with it, plus a couple of precedence traps that bite if you drop the wrong parentheses. Property hooks and lazy objects grabbed the 8.4 headlines, but alongside the array_find family this is the quieter ergonomics win I use almost daily.

What changed in PHP 8.4: new without parentheses#

Class member access on instantiation has existed since PHP 5.4 — but only if you wrapped the new expression in parentheses. Read a property, call a method, or grab a constant off a freshly built object and you paid the parenthesis tax every time:

// Before — PHP 8.3 and earlier
$name = (new ReflectionClass($model))->getShortName();

// After — PHP 8.4
$name = new ReflectionClass($model)->getShortName();

The RFC makes this work because the parser treats the first thing after new as a class name, and the constructor's parentheses mark where that class name ends. Once the parser sees new ReflectionClass($model), it knows the object is fully built and whatever follows — ->, ::, () or [] — applies to that object. No ambiguity, and the RFC ships with zero backward-compatibility breaks: any code valid before 8.4 keeps its exact meaning.

What you can chain off new without parentheses#

This is not limited to instance methods. Every form of member access works, as long as the constructor parentheses are there. Take this class:

final class Money
{
    public const CURRENCY = 'GBP';

    public static string $locale = 'en_GB';

    public function __construct(public int $pence = 0) {}

    public function formatted(): string
    {
        return number_format($this->pence / 100, 2);
    }

    public static function zero(): self
    {
        return new self();
    }

    // Lets you call the object like a function
    public function __invoke(): int
    {
        return $this->pence;
    }
}

Every one of these is valid PHP 8.4:

new Money(500)->formatted();   // instance method  -> "5.00"
new Money(500)->pence;         // instance property -> 500
new Money()::CURRENCY;         // class constant    -> "GBP"
new Money()::$locale;          // static property   -> "en_GB"
new Money()::zero();           // static method     -> Money instance
new Money(500)();              // __invoke()        -> 500

Dynamic class names work too, which is handy in factories where the class is resolved at runtime:

$class = Money::class;

new $class(500)->formatted();  // "5.00"

So does array access, when the object is array-like (for example a class extending ArrayObject), and anonymous classes — which are the one case where you can even omit the constructor parentheses:

// Anonymous class, no constructor args needed
$result = new class {
    public function handle(): string
    {
        return 'done';
    }
}->handle();

The one rule: keep the constructor parentheses#

Here is the single thing to remember. The parentheses after the class name are mandatory, even when the constructor takes no arguments. They are what tells the parser the class name has ended:

new Money()->formatted();   // ✅ works
new Money->formatted();     // ❌ Parse error: unexpected token "->"

The RFC author calls the argument parentheses "crucial" for exactly this reason — strip them and the parser has no idea where your class name stops and your method call starts.

Gotchas and Edge Cases#

The traps all come from the same place: when there are no constructor parentheses, the old precedence rules still apply, and they mean something different from what the new syntax might lead you to expect. These four lines are unchanged from older PHP:

new Money::CURRENCY;        // still a parse error
new Money::$locale;         // parsed as new (Money::$locale)
new $service->factory;      // parsed as new ($service->factory)
new $registry['mailer'];    // parsed as new ($registry['mailer'])

Read new Money::$locale carefully: without constructor parens, PHP resolves Money::$locale first and treats its value as the class name to instantiate. That is the pre-8.4 behaviour, and it is deliberately preserved so existing code does not change meaning. Add the parentheses — new Money()::$locale — and you get the new behaviour: read the static property off a freshly built Money. The presence or absence of () flips the entire interpretation.

The second "gotcha" is taste, not syntax. Just because you can drop the wrapper does not mean every chain reads better without it. For a single short call I prefer the lean form. For a long fluent builder, the wrapping parentheses sometimes do useful work signalling "this whole expression is one freshly constructed object." It is a judgement call, and consistency across your team matters more than which side you land on.

Tooling: PHPStan, Pint, and Rector#

Static analysis caught up quickly. PHPStan parses the syntax fine because it sits on top of nikic/php-parser, which added support in 5.1.0 (July 2024). On a current version you will not see false positives, so this plays nicely with running PHPStan at level 10.

Formatting is the part to watch. PHP-CS-Fixer — which Laravel Pint wraps — has a new_with_parentheses rule, but that only governs the constructor argument parentheses (whether you write new Foo or new Foo()). A dedicated fixer to strip the outer (new Foo())-> parentheses is still in development, so do not expect Pint to rewrite your codebase for you yet. When you do want a codebase-wide migration to the new style, a refactoring tool like Rector is the right hammer for that job.

One last check: this is 8.4-only syntax. If your composer.json still allows PHP 8.3, the file will not even parse on those runtimes, so bump your minimum version before you sprinkle it everywhere.

Wrapping Up#

Adopt it for new code on 8.4: drop the wrapper, keep the constructor parentheses, and stay alert to the no-paren precedence cases where ::, -> and [] still bind the old way. It is a small change that quietly removes a lot of visual noise from builders and factories. If cleaner chaining is your thing, the pipe operator in PHP 8.5 takes the same idea further and is worth a look next.

FAQ#

What is 'new without parentheses' in PHP 8.4?

It is a syntax change that lets you access members on a freshly instantiated object without wrapping the new expression in parentheses. Where PHP 8.3 required (new Foo())->bar(), PHP 8.4 accepts new Foo()->bar() directly. It applies to methods, properties, constants, static members, array access, and invoking the object.

Do I still need parentheses after the class name in PHP 8.4?

Yes, and this is the key rule. The constructor parentheses are mandatory even when there are no arguments, because they tell the parser where the class name ends. new Foo()->bar() is valid, but new Foo->bar() is a parse error. The only exception is anonymous classes with no constructor arguments.

Can I chain properties and static methods on a new instance?

Yes. PHP 8.4 supports the full range of member access off new: instance properties (new Foo()->prop), static properties (new Foo()::$prop), class constants (new Foo()::CONSTANT), static methods (new Foo()::make()), and even invoking the object with new Foo()(). Dynamic class names like new $class()->method() work too.

Does PHPStan support new without parentheses?

Yes. PHPStan relies on nikic/php-parser, which added support for the syntax in version 5.1.0 in July 2024, so a reasonably current PHPStan install analyses it without false positives. PHP-CS-Fixer and Laravel Pint can run on 8.4 code, but they do not yet have a fixer to automatically add or remove the outer parentheses for you.

Why use new without parentheses?

It removes visual noise. Builder and configurator code is full of (new X())->... chains, and stripping the wrapper makes them read more like the equivalent in Java, C#, or TypeScript. It is purely a readability improvement — there is no runtime difference — so the upside is cleaner one-liners with one rule to remember.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.