Add support for first class callables

This commit is contained in:
Fabien Potencier
2024-07-14 10:34:34 +02:00
parent 5036ab2a4a
commit d2eab12e70
4 changed files with 51 additions and 2 deletions
+4 -2
View File
@@ -25,6 +25,7 @@ use Twig\Node\NodeOutputInterface;
use Twig\Node\PrintNode; use Twig\Node\PrintNode;
use Twig\Node\TextNode; use Twig\Node\TextNode;
use Twig\TokenParser\TokenParserInterface; use Twig\TokenParser\TokenParserInterface;
use Twig\Util\ReflectionCallable;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
@@ -156,8 +157,9 @@ class Parser
if (null !== $test) { if (null !== $test) {
$e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
if (\is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) { $callable = (new ReflectionCallable($test))->getCallable();
$e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno)); if (\is_array($callable) && $callable[0] instanceof TokenParserInterface) {
$e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $callable[0]->getTag(), $lineno));
} }
} else { } else {
$e = new SyntaxError(\sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); $e = new SyntaxError(\sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
+2
View File
@@ -55,8 +55,10 @@ final class ReflectionCallable
$callable = [$object, $r->name]; $callable = [$object, $r->name];
$this->name = get_debug_type($object).'::'.$r->name; $this->name = get_debug_type($object).'::'.$r->name;
} elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) {
$callable = [$class->name, $r->name];
$this->name = $class->name.'::'.$r->name; $this->name = $class->name.'::'.$r->name;
} elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) {
$callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name];
$this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; $this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name;
} else { } else {
$callable = $this->name = $r->name; $callable = $this->name = $r->name;
+11
View File
@@ -43,6 +43,9 @@ class FilterTest extends NodeTestCase
$environment->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true])); $environment->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true]));
$environment->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true])); $environment->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true]));
$environment->addFilter(new TwigFilter('magic_static', __NAMESPACE__.'\ChildMagicCallStub::magicStaticCall')); $environment->addFilter(new TwigFilter('magic_static', __NAMESPACE__.'\ChildMagicCallStub::magicStaticCall'));
if (\PHP_VERSION_ID >= 80111) {
$environment->addExtension(new FilterTestExtension());
}
$extension = new class() extends AbstractExtension { $extension = new class() extends AbstractExtension {
public function getFilters(): array public function getFilters(): array
@@ -121,6 +124,14 @@ class FilterTest extends NodeTestCase
$node = $this->createFilter($string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); $node = $this->createFilter($string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]);
$tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", null, "bar")', $environment]; $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", null, "bar")', $environment];
if (\PHP_VERSION_ID >= 80111) {
$node = $this->createFilter($string, 'first_class_callable_static');
$tests[] = [$node, 'Twig\Tests\Node\Expression\FilterTestExtension::staticMethod("abc")', $environment];
$node = $this->createFilter($string, 'first_class_callable_object');
$tests[] = [$node, '$this->extensions[\'Twig\Tests\Node\Expression\FilterTestExtension\']->objectMethod("abc")', $environment];
}
$node = $this->createFilter($string, 'barbar', [ $node = $this->createFilter($string, 'barbar', [
new ConstantExpression('1', 1), new ConstantExpression('1', 1),
new ConstantExpression('2', 1), new ConstantExpression('2', 1),
@@ -0,0 +1,34 @@
<?php
namespace Twig\Tests\Node\Expression;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class FilterTestExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('first_class_callable_static', self::staticMethod(...)),
new TwigFilter('first_class_callable_object', $this->objectMethod(...)),
];
}
public static function staticMethod()
{
}
public function objectMethod()
{
}
}