Skip to content

Fix support for named closures #3702

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

Merged
merged 1 commit into from
May 15, 2022

Conversation

nicolas-grekas
Copy link
Contributor

@@ -149,8 +149,6 @@ public function addExtension(ExtensionInterface $extension)
throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class));
}

// For BC/FC with namespaced aliases
$class = (new \ReflectionClass($class))->name;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not needed because $class = \get_class($extension), which means aliases are already resolved

$closingParenthesis = true;
$isArray = true;
$compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), ', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
$compiler->raw(sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

PHP 7.1 means we can remove using call_user_func_array() and use an inline call instead.

@@ -245,10 +236,7 @@ protected function normalizeName($name)

private function getCallableParameters($callable, bool $isVariadic): array
{
list($r) = $this->reflectCallable($callable);
if (null === $r) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

a reflection object is now always returned, even for magic methods

Copy link
Member

@stof stof left a comment

Choose a reason for hiding this comment

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

This needs to add tests in FunctionTest showing that using Closure::fromCallable on an extension method actually compiles to the optimized $this->extensions['extension_class_name']->method() call rather than to $this->env->getFunction('foo')->getCallable() which triggers a full initialization of extensions.

@nicolas-grekas nicolas-grekas force-pushed the first-class-callable branch 2 times, most recently from 8ec8522 to 5a13c52 Compare May 13, 2022 13:24
@nicolas-grekas
Copy link
Contributor Author

Test added.

@stof
Copy link
Member

stof commented May 13, 2022

The tests you added don't cover the case of calling extension methods

@nicolas-grekas nicolas-grekas force-pushed the first-class-callable branch from 5a13c52 to 163f074 Compare May 13, 2022 15:02
@nicolas-grekas
Copy link
Contributor Author

Test added :)

@fabpot
Copy link
Contributor

fabpot commented May 15, 2022

Thank you @nicolas-grekas.

@LukeTowers
Copy link

@nicolas-grekas this PR has caused an issue with callables that point to Laravel facades in Winter CMS: wintercms/winter#557 (comment). Do you have any idea what would be causing that and how we can go about fixing it?

@nicolas-grekas nicolas-grekas deleted the first-class-callable branch June 20, 2022 14:24
@nicolas-grekas
Copy link
Contributor Author

It looks the code should be changed like this:

--- a/src/Node/Expression/CallExpression.php
+++ b/src/Node/Expression/CallExpression.php
@@ -305,8 +305,8 @@ abstract class CallExpression extends AbstractExpression
             $callable = [$object, $r->name];
             $callableName = (\function_exists('get_debug_type') ? get_debug_type($object) : \get_class($object)).'::'.$r->name;
         } elseif ($class = $r->getClosureScopeClass()) {
-            $callable = [$class, $r->name];
-            $callableName = $class.'::'.$r->name;
+            $callable = [$class->name, $r->name];
+            $callableName = $class->name.'::'.$r->name;
         } else {

Would you be available to send this with a test case, please?

@LukeTowers
Copy link

LukeTowers commented Jun 20, 2022

Off the top of my head, it would probably look something like this:

class CallStaticCallable
{
    public static function __callStatic($name, $arguments)
    {
        if ($name === 'magicMethod') {
            return 'mischief managed';
        }
    }
}

$callable = [CallStaticCallable::class, 'magicMethod'];

Is that enough for you to go off of @nicolas-grekas?

@nicolas-grekas
Copy link
Contributor Author

That should definitely help. Would you be up to submit a PR with everything wrapped up?

@LukeTowers
Copy link

At the moment I don't have the time unfortunately, however perhaps @mjauvin, @damsfx, @WebVPF, or @arvislacis would be willing to as the issue is directly impacting them.

@bennothommo
Copy link
Contributor

bennothommo commented Jun 21, 2022

@nicolas-grekas here's a patch that has the full test case:

diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php
index ef7c82cb..1a86e800 100644
--- a/tests/Node/Expression/FilterTest.php
+++ b/tests/Node/Expression/FilterTest.php
@@ -49,6 +49,7 @@ class FilterTest extends NodeTestCase
                 return [
                     new TwigFilter('foo', \Closure::fromCallable([$this, 'foo'])),
                     new TwigFilter('foobar', \Closure::fromCallable([$this, 'foobar'])),
+                    new TwigFilter('alias_bar', ['Twig_Test_Alias_Class', 'test']),
                 ];
             }
 
@@ -135,6 +136,10 @@ class FilterTest extends NodeTestCase
         $node = $this->createFilter($string, 'foobar');
         $tests[] = [$node, '$this->env->getFilter(\'foobar\')->getCallable()("abc")', $environment];
 
+        // aliased class
+        $node = $this->createFilter($string, 'alias_bar');
+        $tests[] = [$node, '$this->env->getFilter(\'alias_bar\')->getCallable()("abc")', $environment];
+
         return $tests;
     }
 
@@ -190,3 +195,31 @@ function twig_tests_filter_dummy()
 function twig_tests_filter_barbar($context, $string, $arg1 = null, $arg2 = null, array $args = [])
 {
 }
+
+// Sample class with a magic method
+class Twig_Test_Parent_Class
+{
+    public static function getAccessor()
+    {
+        throw new \Exception('Accessor not set');
+    }
+
+    public static function __callStatic($name, $arguments)
+    {
+        $accessor = static::getAccessor();
+
+        if ($name === 'test') {
+            return 'received ' . $arguments[0];
+        }
+    }
+}
+
+class Twig_Test_Child_Class extends Twig_Test_Parent_Class
+{
+    public static function getAccessor()
+    {
+        return 'child';
+    }
+}
+
+class_alias(Twig_Test_Child_Class::class, 'Twig_Test_Alias_Class');

It passes in the test case, in the sense that it creates the compiled getFilter call just fine - the issue would only manifest if one were to run the actual template.

What seems to happen is that the $r->getClosureScopeClass() call here returns the class as Twig_Test_Parent_Class. I assume this is because that is the class that will actually fulfill the call. The problem is calling that class directly would fail because of the getAccessor method, whereas calling it in the context of the child class (or the alias) would be fine normally. This is similar to how Laravel facades would work.

I hope that makes sense - let me know if you need any further info.

@stof
Copy link
Member

stof commented Jun 21, 2022

Could you open an issue with the details instead of commenting on a merged PR ? It would avoid loosing track of the issue.

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

Successfully merging this pull request may close these issues.

None yet

5 participants