Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new MyClass()->method() without parentheses #13029

Closed

Conversation

vudaltsov
Copy link
Contributor

@vudaltsov vudaltsov commented Dec 26, 2023

This PR allows to immediately access newly created object without parentheses.

class Request implements Psr\Http\Message\RequestInterface
{
    // ...
}

// BEFORE
$request = (new Request())->withMethod('GET')->withUri('/hello-world');

// AFTER
$request = new Request()->withMethod('GET')->withUri('/hello-world');

This was requested and discussed several times, see:

Here's what you will be able to write after this change:

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'; }
}

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(
    // 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'; } }(),
);

Parentheses still cannot be omitted around the new expression without constructor arguments' parentheses in non-anonymous classes, 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](http://www.php.net/constant);
new MyClass::$staticProperty;
new $myClass::[CONSTANT](http://www.php.net/constant);
new $myClass::$staticProperty;
new $myClass->property;
new $myClass->method();

For more details see the RFC: https://wiki.php.net/rfc/new_without_parentheses

TODO:

  • Regular class tests.
  • Anonymous class tests.
  • No constructor arguments' parentheses tests.

@oleg-andreyev
Copy link

Idk. With parentheses it looks more logical and clear, like in math, first is done what’s enclosed in parentheses and later the rest.

@Wulfheart
Copy link

Please turn this into an RFC! I hate the fact that I hace to put parentheses after a new.

@TimWolla
Copy link
Member

Ready to make an RFC if this PR makes sense.

Syntax changes most certainly need to go through the RFC process, due to the impact on IDEs and static analysis tools that need to learn about the new syntax.

Copy link

@caendesilva caendesilva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that to me any many others, the current syntax looks visually cleaner and is less ambiguous. However, that is probably due to the fact that we are all so used to having to add parentheses.

Instead of asking "Do I like this syntax?", the better question to ask might instead be "Do I wish that this is how the syntax had always been?".

When I was new to PHP, I distinctly remember having issues with this and being confused why I needed to put the class construction in parentheses just to chain methods on it. As a PHP beginner, I expected things to work like this change proposes, so I think that this change would indeed be beneficial to newcomers.

The question then remains, are the rest of us who have been using the old syntax for so long ready for this change? That is a question I alone cannot answer, and why this definitely needs an official RFC proposal.

Furthermore, we must also consider the potential for ambiguity that could arise from the proposed syntax. Even if the implementation solves for the ambiguity (thread), there is another factor to consider, one that is harder to solve for using code: how do developers mentally evaluate if new Something() refers to a function or an object?

@medabkari
Copy link

When I was new to PHP, I distinctly remember having issues with this and being confused why I needed to put the class construction in parentheses just to chain methods on it. As a PHP beginner, I expected things to work like this change proposes, so I think that this change would indeed be beneficial to newcomers.

+1

I experienced the same when I started learning PHP, and I believe many others did. Therefore, this change could make life easier for newcomers.

@Neirda24
Copy link

new Something()->getSomething() if something is a function then previously it would use the function return as argument to new. With the new syntax it would become an object having a method getSomething that should return a string. The thing is with parenthesis it makes the precedence clear and future proof. Without I fear the maintenance might become tedious to debug and stuff.

Also the examples doesn't show the case where () are missing from classes :

new A->method(). In this case the cognitive load is to look if A is prefixed with a $ or not to understand the precedence on this.

For what my opinion is worth : I don't think changing this could be easier on long terms. Maybe for new comers ot the php world. But at some point we all have to maintain code and this can heavily complexify things IMO.

@caendesilva
Copy link

new Something()->getSomething() if something is a function then previously it would use the function return as argument to new. With the new syntax it would become an object having a method getSomething that should return a string. The thing is with parenthesis it makes the precedence clear and future proof. Without I fear the maintenance might become tedious to debug and stuff.

Also the examples doesn't show the case where () are missing from classes :

new A->method(). In this case the cognitive load is to look if A is prefixed with a $ or not to understand the precedence on this.

For what my opinion is worth : I don't think changing this could be easier on long terms. Maybe for new comers ot the php world. But at some point we all have to maintain code and this can heavily complexify things IMO.

These are my overall concerns as well. Using new A->method() makes me intuitively think that this is a constant with a class name being instantiated. I'm worried that the cognitive load as you eloquently put it may far outweigh any syntactical sugar provided by the change. Note that I'm not at all against this, I do like the syntax, but I am questioning whether or not the change is worth it and actually provides value and reduces complexity in the long term.

@vudaltsov
Copy link
Contributor Author

Also the examples doesn't show the case where () are missing from classes : new A->method()

Without parentheses PHP behaves in the same way it currently does: throws a syntax error. I will elaborate that in the RFC soon.

@caendesilva
Copy link

Also the examples doesn't show the case where () are missing from classes : new A->method()

Without parentheses PHP behaves in the same way it currently does: throws a syntax error. I will elaborate that in the RFC soon.

I personally don't think that makes sense. (new Foo()) and (new Foo) are both valid. So why shouldn't new Foo() and new Foo be the same?

@psihius
Copy link

psihius commented Dec 26, 2023

How are you going to treat the ambiguity?

class Test 
{
   public function method(): string
   {
       return DateTime::class;
   }
}

$result = new Test()->method();

// Is it this where this results in  $result being a string "DateTime"
$result = (new Object())->method();

// or is it going to be this, where $result ends up being a new DateTime object?
$result = (new Object()->method());

@vudaltsov
Copy link
Contributor Author

@caendesilva , you won't be able to do new A->method(). Either (new A)->method() or new A()->method().

That's because PHP supports dynamic classes in new expressions. If you have new $class->method() — it's an ambiguity. If you have (new $class)->method() or new $class()->method(), it isn't.

@vudaltsov
Copy link
Contributor Author

@psihius, there's no ambiguity in your example if I get it right.

new Test()->method(); is (new Test())->method(); and result is 'DateTime'. There are no other ways to interpret it: according to Operator Precedence new has the highest precedence, it will be evaluated first.

Also note that the bison grammar changes compiled without errors or notices, which proves that it is unambiguous.

@caendesilva
Copy link

Idk. With parentheses it looks more logical and clear, like in math, first is done what’s enclosed in parentheses and later the rest.

I'm inclined to agree. However, do we feel this way just because we are used to seeing the current implementation? It's hard take an objective look at something so subjective as code styles.

@vudaltsov
Copy link
Contributor Author

@TimWolla , I will create an RFC by the end of the week, thank you!

@TimWolla
Copy link
Member

@vudaltsov Unless you already have wiki / RFC karma, I recommend requesting it early. There's a limited number of folks who can grant that karma and I would not be surprised if some of them are on vacation currently.

@caendesilva
Copy link

Also note that the bison grammar changes compiled without errors or notices, which proves that it is unambiguous.

Remember that even if the implementation is unambiguous, the way developers' brains mentally parses the code may still be ambiguous and unexpected.

By the way, @vudaltsov, the reason for me making these types of comments is not to tear you down or to criticise the code and your hard work. It's my good faith effort to come with constructive criticism to find and resolve eventual pitfalls that could lead to this proposal being rejected.

@psihius
Copy link

psihius commented Dec 26, 2023

@psihius, there's no ambiguity in your example if I get it right.

new Test()->method(); is (new Test())->method(); and result is 'DateTime'. There are no other ways to interpret it: according to Operator Precedence new has the highest precedence, it will be evaluated first.

Also note that the bison grammar changes compiled without errors or notices, which proves that it is unambiguous.

I do not like the fact that new A()->bruh() will work and new A->bruh() will not, since (new DateTime)->format(...); and (new DateTime())->format(...);` are valid. It breaks syntax consistency and introduces a WAT

@iluuu1994
Copy link
Member

Note that GitHub is not the intended medium for non-technical discussions. Please use the internals mailing list instead. I realize there's no thread for this topic yet, the first step would be for @vudaltsov to start one.

@caendesilva

This comment was marked as off-topic.

@TimWolla

This comment was marked as off-topic.

@caendesilva

This comment was marked as off-topic.

@indigoram89
Copy link

Cool! I like it!

@St-Sinner
Copy link

the next simplification is
new new MyClass()->method()()

@carlos-granados
Copy link

The good thing about this proposal is that it does not force you to use the new syntax. The old syntax would still be perfectly valid. So you can choose the one that you prefer, instead of being forced to use the first one. +100 from me

@pelmered
Copy link

pelmered commented Dec 27, 2023

I think it would be a better idea to just add a default static constructor to all PHP classes that could be overrided if you want. Like this:

class A
{
   public static new(...$args) // This would be added automatically to all classes
   {
        return new self(...$args);
   }

   const CONSTANT = 'constant';
   public static $staticProperty = 'staticProperty';
   public static function staticMethod() {}
   public $property = 'property';
   public function method() {}
   public function __invoke() {}
}

A::new()::CONSTANT;
A::new()::$staticProperty;
A::new()::staticMethod();
A::new()->property;
A::new()->method();
A::new()();

This would be clean, concise and avoids any ambiguity.
Since it is overridable, it is also fully backwards compatible.

@Wirone

This comment was marked as off-topic.

@vudaltsov vudaltsov force-pushed the new_obj_access_without_parentheses branch from edbb91d to b95ecd8 Compare May 26, 2024 19:27
@vudaltsov vudaltsov force-pushed the new_obj_access_without_parentheses branch from b95ecd8 to d8ef6d5 Compare May 26, 2024 20:08
@vudaltsov
Copy link
Contributor Author

@nikic, thank you very much for your review! I've just fixed the implementation:

  • new_dereferenceable is now explicitly mentioned in all relevant expressions, not in callable_variable
  • added tests to ensure that unset(new A()); and new A() = 1; produce a syntax error
  • added tests for new ArrayAccessClass()['key']
  • added array access examples to RFC
  • grouped tests as you have recommended

@vudaltsov
Copy link
Contributor Author

So, how to call invokables now?

@olamedia, nothing has changed for invokables, see tests and RFC. Please, post a snippet that you consider prolematic.

@@ -1324,6 +1330,8 @@ function_call:
{ $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $4); }
| variable_class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list
{ $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $4); }
| new_dereferenceable T_PAAMAYIM_NEKUDOTAYIM member_name argument_list
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need to list new_dereferenceable in quite this many places -- just having it in fully_dereferenceable and callable_expr should be enough. Something along these lines seems to work: https://gist.github.com/nikic/375e4a3a0c0adb7911ac75200c8f45c7

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thank you, that's much cleaner.

Copy link
Member

@nikic nikic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me now, thanks!

Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I will merge this shortly, thank you for your RFC!

@iluuu1994 iluuu1994 closed this in b6b16a1 May 27, 2024
@iluuu1994
Copy link
Member

@vudaltsov Feel free to mark the RFC as implemented. I also noticed that you didn't announce the successful vote (https://externals.io/message/123293). It's not technically required by the RFC process (apparently, at least https://wiki.php.net/rfc/howto doesn't mention it), but it's almost universally done so that internals are aware of the result.

@Wirone
Copy link

Wirone commented May 27, 2024

Why is it closed as not merged? I know it landed, but why technically in Github it has invalid resolution? Is it a php-src convention or what?

@vudaltsov congrats and thanks for your work 🙂.

@iluuu1994
Copy link
Member

Because it was rebased and closed via Closes GH-xxx commit message.

@Wirone
Copy link

Wirone commented May 27, 2024

In PHP-CS-Fixer we also enforce linear history, so we rebase and squash PRs, but these are marked as merged anyway (as we do this within Github, using its controls). Isn't it possible to achieve this for the php-src workflow? It just looks wrong...

Disclaimer: I don't know the full process, so I'm probably missing something. I am just curious, because such details are important to me 😅.

@iluuu1994
Copy link
Member

For the PR to appear as merged, you either need to merge/squash via GitHub UI, or you need to push to the branch again to make the commit hashes match. The former works fine, unless you have to make changes (like add an UPGRADING entry in this case). The latter wastes CI.

@vudaltsov vudaltsov deleted the new_obj_access_without_parentheses branch May 28, 2024 17:27
@derickr
Copy link
Member

derickr commented May 29, 2024

This seems to have broken one of Xdebug's tests, through a change in syntax error.

Not normally an issue, but I think this new message is not better.

<?php
class DB 
{
    function f()
    {
        if (true) {}
        $obj =& new a;
    }
}
?>

This script used to throw:

Parse error: syntax error, unexpected token "new" in /home/derick/dev/php/xdebug-xdebug/tests/coverage/bug00422.inc on line 7

But now it throws:

Parse error: syntax error, unexpected token ";", expecting "(" in /home/derick/dev/php/xdebug-xdebug/tests/coverage/bug00422.inc on line 7

Especially the expecting "(" is odd.

@nikic
Copy link
Member

nikic commented May 29, 2024

@derickr This change looks reasonable to me, because $obj =& new a()->x; for example would turn that into valid code. So ( is indeed the only possible token there :)

@iluuu1994
Copy link
Member

Although $obj =& new a()->foo; itself is still forbidden in the compiler:

Cannot use temporary expression in write context

But this is certainly not the only place where the parser can make suggestions that end up not being valid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.