Skip to content

Fix GH-8932: Wrong closure scope class reported for static methods #8935

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

Closed
wants to merge 3 commits into from

Conversation

cmb69
Copy link
Member

@cmb69 cmb69 commented Jul 6, 2022

ReflectionFunction::getClosureScopeClass() needs to report the
called_scope, if there is one.

Unfortunately, it is not yet possible to retrieve that value from the
closure object, so we need to introduce a getter.


Frankly, I don't know wether the called_scope can ever be NULL. If not, we won't need the fallback to closure_func->common.scope. And I'm not sure whether it might be preferable to catch the called_scope during ReflectionFunction::__construct(), so we won't need the getter.

`ReflectionFunction::getClosureScopeClass()` needs to report the
`called_scope`, if there is one.

Unfortunately, it is not yet possible to retrieve that value from the
closure object, so we need to introduce a getter.
@mvorisek
Copy link
Contributor

mvorisek commented Jul 6, 2022

can this target PHP 8.0?

$c = ['B', 'b'];
$d = \Closure::fromCallable($c);
$r = new \ReflectionFunction($d);
var_dump($r->getClosureScopeClass());
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is to be expected; since there is no closure, there is no closure scope class. But in any way, ReflectionFunctionAbstract::getClosureScopeClass() needs proper documentation.

@arnaud-lb
Copy link
Member

Zend/tests/closure_042.phpt is failing, but I think that the new output is the expected one

Frankly, I don't know wether the called_scope can ever be NULL. If not, we won't need the fallback to closure_func->common.scope. And I'm not sure whether it might be preferable to catch the called_scope during ReflectionFunction::__construct(), so we won't need the getter.

We could avoid the getter and the common.scope fallback by using the object's get_closure handler. Since this is what the engine uses to determine the called scope, this will ensure that ReflectionFunction::getClosureScopeClass() remains consistent with the engine.

@cmb69
Copy link
Member Author

cmb69 commented Jul 15, 2022

Zend/tests/closure_042.phpt is failing, but I think that the new output is the expected one

That is what confuses me. That test explicitly assumes that the dummy sope[sic] is Closure:

var_dump($rm->getClosureScopeClass()->name); //dummy sope is Closure

Wrong test expectation? Maybe, but changing that might break code which relies on that behavior.

@nicolas-grekas
Copy link
Contributor

nicolas-grekas commented Jul 29, 2022

Wrong test expectation?

I think so yes.

might break code which relies on that behavior

Every bugfix has this potential :) The current behavior isn't useful.

@nicolas-grekas
Copy link
Contributor

can this target PHP 8.0?

I second that, this is a bugfix to me.

cmb69 added 2 commits August 1, 2022 17:25
That dummy scope doesn't seem to make sense here.  Maybe the test was
just checking the erroneous implementation.
@cmb69
Copy link
Member Author

cmb69 commented Aug 1, 2022

We could avoid the getter and the common.scope fallback by using the object's get_closure handler.

Ah, right. Thank you! I've did this now.

I'm still somewhat reluctant to target PHP-8.0, because of that closure_042.phpt.

@cmb69 cmb69 marked this pull request as ready for review August 2, 2022 17:50
@arnaud-lb
Copy link
Member

Looking more closely, I'm wondering whether getClosureScopeClass() is supposed to return the called_scope or the scope.

In my understanding, the static keyword and $this refer to called_scope, visibility checks during execution of the function are made against scope, and the self keyword refers to scope.

If getClosureScopeClass() is supposed to return the scope, both closure_042.phpt and the behavior observed in #8932 was right:

Also, the only other place where we use the word scope in the API is in the newScope parameter of Closure::bindTo() and Closure::bind(). It affects the scope of the closure, while the newThis parameter affects the called_scope.

I think that we should introduce a new function to expose the called_scope.

@nicolas-grekas
Copy link
Contributor

I can testify that yes, for named closures, the current behavior has introduced very subtle and unexpected bugs, impossible to anticipate without intimate understanding of the engine, and more importantly impossible to fix.

See eg twigphp/Twig#3722

@arnaud-lb
Copy link
Member

We can think of of scope as self, and called_scope as static.

getClosureScopeClass() returns the self of a Closure: https://3v4l.org/ktqXB

I agree that we need a way to expose called_scope / static. E.g. a new method getClosureCalledScopeClass() that would return Foo for Closure::createFromCallable("Foo::bar")).

Maybe we could add this to 8.0 and 8.1 as well.

@nicolas-grekas
Copy link
Contributor

The thing is: the "self" for a named closure serves no purpose. This is never a useful info since nothing can be built out of it. And reciprocally for anonymous closure, there is no "static" scope. For this reason, the current fix looks the correct one to me.

@bwoebi
Copy link
Member

bwoebi commented Aug 11, 2022

Given class A { public static function F() { return "A"; } }

The difference is https://3v4l.org/SuAuh:

(function() { var_dump(static::F()); })->bindTo(new A, NULL)();
vs
(function() { var_dump(self::F()); })->bindTo(new A, NULL)();
vs
(function() { var_dump(self::F()); })->bindTo(new A, "A")();

Basically: "self" scope of closure = second argument to bindTo, static scope of closure = scope of $this, if $this is given, otherwise static = self.

The only way to get the self scope of the closure for anonymous closures is (new \ReflectionFunction($c))->getClosureScopeClass(). As such, changing the behaviour of closure_042.phpt is wrong.

Now, this static scope of closure = scope of $this rule is only valid for anonymous closures. For named closures, the static scope of the closure may be independent, and is currently not accessible. As such I support @arnaud-lb idea to add a new function for this case in PHP-8.0+.

@nicolas-grekas Why is "self" of a named closure not useful information? How would you then get the defining class of a named closure (of a static method)?

@nicolas-grekas
Copy link
Contributor

nicolas-grekas commented Aug 11, 2022

In all cases where I've needed this, I first have a branch based on getClosuseThis. When I enter this branch, I'd use ReflectionMethod($this, 'method')->getDeclaringClass(), so it's very natural to do the same for static methods.

If we're up to adding a new method, may I suggest toCallable()? This would turn back named closures into either a string or an array, and this would return anonymous closures as is. That would be super helpful (I've already written logic for this a couple of times in several libs.)

@bwoebi
Copy link
Member

bwoebi commented Aug 11, 2022

@nicolas-grekas yes, if you have a $this (i.e. a non-static method), then this is viable. Hence I was explicitly saying (of a static method). Maybe you haven't encountered an use case for Closures of static methods.

I would simply add a new function getting the ReflectionClass of the static scope. Writing your own toCallable logic based on that would then be easy.

@nicolas-grekas
Copy link
Contributor

nicolas-grekas commented Aug 11, 2022

My point is: I don't see a use case where getting the declaring class of a static method is useful, in the context of named closures.
Actually, I think there are bugs lying out there that don't know about this behavior, because the only moment where it matters is when constructing a named closure out of a static method on a class that has __callStatic. This is a super niche edge case, yet this is the one Laravel relies on, and how I spotted this issue (see twigphp/Twig#3722)

@cmb69
Copy link
Member Author

cmb69 commented Aug 11, 2022

Thanks for the explanation, @arnaud-lb and @bwoebi. LSB is such a nice feature. ;) Anyhow, I'm closing this PR, as it is obviously wrong.

I agree that a new method makes sense, but it seems to me that adding a public method to ReflectionFunctionAbstract() constitutes a BC break.

@cmb69 cmb69 closed this Aug 11, 2022
@cmb69 cmb69 deleted the cmb/gh8932 branch August 11, 2022 14:29
@nicolas-grekas
Copy link
Contributor

I'm sorry but I don't understand the rationale. What's the "scope" of a closure if it's not what static::class returns? The current behavior is just buggy...

@bwoebi
Copy link
Member

bwoebi commented Aug 11, 2022

@nicolas-grekas It's self::class. I think there is a bit a naming problem... Internally we have scope and called_scope. The former being self, the latter static. I think the naming has been applied 1:1 to the reflection method name here and confused you probably.

@cmb69
Copy link
Member Author

cmb69 commented Aug 11, 2022

The super verbose documentation of ReflectionFunctionAbstract::getClosureScopeClass() may add to the confusion. ;)

@nicolas-grekas
Copy link
Contributor

Got it thanks. I submitted #9299 as a follow up.

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.

Wrong closure scope class reported for static methods
5 participants