rfc:new_without_parentheses

PHP RFC: new MyClass()->method() without parentheses

Introduction

The “class member access on instantiation” feature was introduced in PHP 5.4.0. Since then constants, properties and methods can be accessed on a newly created instance without an intermediate variable, but only if the new expression is wrapped in parentheses:

class Request implements Psr\Http\Message\RequestInterface
{
    // ...
}
 
// OK
$request = (new Request())->withMethod('GET')->withUri('/hello-world');
 
// PHP Parse error: syntax error, unexpected token "->"
$request = new Request()->withMethod('GET')->withUri('/hello-world');

The goal of this RFC is to enable the second syntax to:

Proposal

This RFC allows to omit parentheses around the new expression when constructor arguments' parentheses are present. Given class

class MyClass
{
    const CONSTANT = 'constant';
    public static $staticProperty = 'staticProperty';
    public static function staticMethod(): string { return 'staticMethod'; }
    public $property = 'property';
    public function method(): string { return 'method'; }
    public function __invoke(): string { return '__invoke'; }
}

one will be able to write

var_dump(
    new MyClass()::CONSTANT,        // string(8)  "constant"
    new MyClass()::$staticProperty, // string(14) "staticProperty"
    new MyClass()::staticMethod(),  // string(12) "staticMethod"
    new MyClass()->property,        // string(8)  "property"
    new MyClass()->method(),        // string(6)  "method"
    new MyClass()(),                // string(8)  "__invoke"
);
 
$myClass = MyClass::class;
var_dump(
    new $myClass()::CONSTANT,        // string(8)  "constant"
    new $myClass()::$staticProperty, // string(14) "staticProperty"
    new $myClass()::staticMethod(),  // string(12) "staticMethod"
    new $myClass()->property,        // string(8)  "property"
    new $myClass()->method(),        // string(6)  "method"
    new $myClass()(),                // string(8)  "__invoke"
);
 
var_dump(
    new (trim(' MyClass '))()::CONSTANT,        // string(8)  "constant"
    new (trim(' MyClass '))()::$staticProperty, // string(14) "staticProperty"
    new (trim(' MyClass '))()::staticMethod(),  // string(12) "staticMethod"
    new (trim(' MyClass '))()->property,        // string(8)  "property"
    new (trim(' MyClass '))()->method(),        // string(6)  "method"
    new (trim(' MyClass '))()(),                // string(8)  "__invoke"
);

This RFC still does not allow to omit parentheses around the new expression without constructor arguments' parentheses, because in some cases this leads to an ambiguity:

// Instantiate and then access the instance or instantiate the result of the expression?
new MyClass::CONSTANT;
new MyClass::$staticProperty;
new $myClass::CONSTANT;
new $myClass::$staticProperty;
new $myClass->property;
new $myClass->method();

This RFC allows to omit parentheses around the new anonymous class expression regardless of whether constructor arguments' parentheses are present or not:

var_dump(
    // string(8) "constant"
    new class { const CONSTANT = 'constant'; }::CONSTANT,
    // string(14) "staticProperty"
    new class { public static $staticProperty = 'staticProperty'; }::$staticProperty,
    // string(12) "staticMethod"
    new class { public static function staticMethod() { return 'staticMethod'; } }::staticMethod(),
    // string(8) "property"
    new class { public $property = 'property'; }->property,
    // string(6) "method"
    new class { public function method() { return 'method'; } }->method(),
    // string(8) "__invoke"
    new class { public function __invoke() { return '__invoke'; } }(),
);

Why the proposed syntax is unambiguous

At first glance new MyClass()->method() expression might seem ambiguous. But according to the same logic new MyClass() is also ambiguous: is it new (MyClass()) or new MyClass? However, new MyClass() is unambiguously interpreted as an instantiation of class MyClass with zero constructor arguments, not an instantiation of MyClass() function call result. This is because PHP interprets the first expression after new as a class name.

Consider also MyClass::new()->method(). It is not interpreted as MyClass::(new()->method()) or MyClass::(new())->method(). It is interpreted as (MyClass::new())->method(). It's natural to assume that MyClass::new() is the first expression to evaluate. The same is true for new MyClass()->method(): expression new MyClass() is also the first to evaluate.

Here's how it looks at the grammar level. The formula for the new expression with explicit constructor arguments' parentheses is T_NEW class_name|new_variable|(expr) ctor_arguments, where new_variable is a variable expression without calls.

new MyClass();
^   ^      ^
|   |      |
|—T_NEW    |—ctor_arguments
    |
    |—class_name
 
 
new $class();
^   ^     ^
|   |     |
|—T_NEW   |—ctor_arguments
    |
    |—new_variable (cannot have calls!)
 
new (trim(' MyClass '))();
^   ^                  ^
|   |                  |
|—T_NEW                |—ctor_arguments
    |
    |(expr)

Once the parser encounters T_NEW, it considers whatever comes next a class name, not a part of another expression. This guarantees that the new expression with arguments' parentheses can be unambiguously used as a separate expression without any additional parentheses. It also becomes clear why new_variable cannot have calls and why arguments' parentheses are crucial for the proposed syntax: parentheses denote the end of the class name and the end of the new expression.

Other syntax ideas

Some of the ideas expressed during the discussion of this RFC are listed below. They are orthogonal to this proposal and require a separate RFC.

Allow to omit the new keyword

Some languages like Kotlin allow to instantiate classes via MyClass() expression. Omitting the “new” keyword in PHP is currently not possible, because classes and functions can have the same name (in Kotlin it is not possible). So, in order to achieve this in PHP, we should first deprecate declaring functions and classes with the same names.

MyClass::new(), MyClass::create()

Introducing a dedicated static constructor like MyClass::new() or MyClass::create() would be a backward compatibility break for already existing static or object methods named new or create with required parameters and/or return type different from static. So this idea also requires some deprecations.

Backward Incompatible Changes

None. Any code that is valid before the change will be valid after and interpreted in the same way.

Proposed PHP Version(s)

PHP 8.4

rfc/new_without_parentheses.txt · Last modified: 2024/04/23 08:28 by vudaltsov