diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index f61adc725..6fc6f66e0 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -34,7 +34,7 @@ abstract class CallExpression extends AbstractExpression if (\is_string($callable) && !str_contains($callable, '::')) { $compiler->raw($callable); } else { - $rc = $this->reflectCallable($callable); + $rc = $this->reflectCallable($twigCallable); $r = $rc->getReflector(); $callable = $rc->getCallable(); @@ -271,7 +271,7 @@ abstract class CallExpression extends AbstractExpression private function getCallableParameters($callable, bool $isVariadic): array { $twigCallable = $this->getAttribute('twig_callable'); - $rc = $this->reflectCallable($callable); + $rc = $this->reflectCallable($twigCallable); $r = $rc->getReflector(); $callableName = $rc->getName(); @@ -309,10 +309,10 @@ abstract class CallExpression extends AbstractExpression return [$parameters, $isPhpVariadic]; } - private function reflectCallable($callable): ReflectionCallable + private function reflectCallable(TwigCallableInterface $callable): ReflectionCallable { if (!$this->reflector) { - $this->reflector = new ReflectionCallable($callable, $this->getAttribute('type'), $this->getAttribute('name')); + $this->reflector = new ReflectionCallable($callable); } return $this->reflector; diff --git a/src/Parser.php b/src/Parser.php index 06d7781e7..28cc8a0e1 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -157,7 +157,7 @@ class Parser if (null !== $test) { $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); - $callable = (new ReflectionCallable($test))->getCallable(); + $callable = (new ReflectionCallable(new TwigTest('decision', $test)))->getCallable(); 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)); } diff --git a/src/TwigCallableInterface.php b/src/TwigCallableInterface.php index 13a10cd3b..2a8ff6116 100644 --- a/src/TwigCallableInterface.php +++ b/src/TwigCallableInterface.php @@ -18,6 +18,8 @@ interface TwigCallableInterface extends \Stringable { public function getName(): string; + public function getType(): string; + public function getDynamicName(): string; /** diff --git a/src/TwigFilter.php b/src/TwigFilter.php index 7eb66f713..70b1f8f3f 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -39,6 +39,11 @@ final class TwigFilter extends AbstractTwigCallable ], $this->options); } + public function getType(): string + { + return 'filter'; + } + public function getSafe(Node $filterArgs): ?array { if (null !== $this->options['is_safe']) { diff --git a/src/TwigFunction.php b/src/TwigFunction.php index d90a7e42c..4a10df95e 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -38,6 +38,11 @@ final class TwigFunction extends AbstractTwigCallable ], $this->options); } + public function getType(): string + { + return 'function'; + } + public function getParserCallable(): ?callable { return $this->options['parser_callable']; diff --git a/src/TwigTest.php b/src/TwigTest.php index 570ffd852..5e58ad8b0 100644 --- a/src/TwigTest.php +++ b/src/TwigTest.php @@ -35,6 +35,11 @@ final class TwigTest extends AbstractTwigCallable ], $this->options); } + public function getType(): string + { + return 'test'; + } + public function needsCharset(): bool { return false; diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index e8dea5217..8811ca9c8 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -17,9 +17,6 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\VariadicExpression; use Twig\Node\Node; use Twig\TwigCallableInterface; -use Twig\TwigFilter; -use Twig\TwigFunction; -use Twig\TwigTest; /** * @author Fabien Potencier @@ -28,20 +25,13 @@ use Twig\TwigTest; */ final class CallableArgumentsExtractor { - private string $type; - private string $name; + private ReflectionCallable $rc; public function __construct( private Node $node, private TwigCallableInterface $twigCallable, ) { - $this->type = match (true) { - $twigCallable instanceof TwigFunction => 'function', - $twigCallable instanceof TwigFilter => 'filter', - $twigCallable instanceof TwigTest => 'test', - default => throw new \LogicException('Unknown callable type.'), - }; - $this->name = $twigCallable->getName(); + $this->rc = new ReflectionCallable($twigCallable); } /** @@ -49,7 +39,6 @@ final class CallableArgumentsExtractor */ public function extractArguments(Node $arguments): array { - $rc = new ReflectionCallable($this->twigCallable->getCallable(), $this->type, $this->name); $extractedArguments = []; $named = false; foreach ($arguments as $name => $node) { @@ -57,7 +46,7 @@ final class CallableArgumentsExtractor $named = true; $name = $this->normalizeName($name); } elseif ($named) { - throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } $extractedArguments[$name] = $node; @@ -65,8 +54,8 @@ final class CallableArgumentsExtractor if (!$named && !$this->twigCallable->isVariadic()) { $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); - if (\count($extractedArguments) < $rc->getReflector()->getNumberOfRequiredParameters() - $min) { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } return $extractedArguments; @@ -74,13 +63,13 @@ final class CallableArgumentsExtractor if (!$callable = $this->twigCallable->getCallable()) { if ($named) { - throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->type, $this->name)); + throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); } - throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->type, $this->name)); + throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); } - [$callableParameters, $isPhpVariadic] = $this->getCallableParameters($rc); + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters(); $arguments = []; $names = []; $missingArguments = []; @@ -100,13 +89,13 @@ final class CallableArgumentsExtractor if (\array_key_exists($name, $extractedArguments)) { if (\array_key_exists($pos, $extractedArguments)) { - throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } if (\count($missingArguments)) { throw new SyntaxError(\sprintf( 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', - $name, $this->type, $this->name, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + $name, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -129,7 +118,7 @@ final class CallableArgumentsExtractor $missingArguments[] = $name; } else { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } } @@ -162,7 +151,7 @@ final class CallableArgumentsExtractor throw new SyntaxError( \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', - \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->type, $this->name, implode(', ', $names) + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names) ), $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() @@ -177,11 +166,9 @@ final class CallableArgumentsExtractor return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); } - private function getCallableParameters(ReflectionCallable $rc): array + private function getCallableParameters(): array { - $r = $rc->getReflector(); - - $parameters = $r->getParameters(); + $parameters = $this->rc->getReflector()->getParameters(); if ($this->node->hasNode('node')) { array_shift($parameters); } @@ -208,7 +195,7 @@ final class CallableArgumentsExtractor array_pop($parameters); $isPhpVariadic = true; } else { - throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $rc->getName(), $this->type, $this->name)); + throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $this->rc->getName(), $this->twigCallable->getType(), $this->twigCallable->getName())); } } diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php index 54384e14b..b4a709399 100644 --- a/src/Util/ReflectionCallable.php +++ b/src/Util/ReflectionCallable.php @@ -11,6 +11,8 @@ namespace Twig\Util; +use Twig\TwigCallableInterface; + /** * @author Fabien Potencier * @@ -22,8 +24,10 @@ final class ReflectionCallable private $callable = null; private $name; - public function __construct($callable, string $debugType = 'unknown', string $debugName = 'unknown') - { + public function __construct( + private TwigCallableInterface $twigCallable, + ) { + $callable = $twigCallable->getCallable(); if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; } @@ -40,7 +44,7 @@ final class ReflectionCallable try { $closure = \Closure::fromCallable($callable); } catch (\TypeError $e) { - throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $debugType, $debugName), 0, $e); + throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $twigCallable->getType(), $twigCallable->getName()), 0, $e); } $this->reflector = $r = new \ReflectionFunction($closure); diff --git a/tests/Node/Expression/CallTest.php b/tests/Node/Expression/CallTest.php index 8486a3c4c..a1ea76fd4 100644 --- a/tests/Node/Expression/CallTest.php +++ b/tests/Node/Expression/CallTest.php @@ -24,7 +24,7 @@ class CallTest extends TestCase { public function testGetArguments() { - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->assertEquals(['U', null], $this->getArguments($node, ['date', ['format' => 'U', 'timestamp' => null]])); } @@ -33,7 +33,7 @@ class CallTest extends TestCase $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date".'); - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['timestamp' => 123456, 'Y-m-d']]); } @@ -42,7 +42,7 @@ class CallTest extends TestCase $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "format" is defined twice for function "date".'); - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'format' => 'U']]); } @@ -51,7 +51,7 @@ class CallTest extends TestCase $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown argument "unknown" for function "date(format, timestamp)".'); - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown' => '']]); } @@ -60,7 +60,7 @@ class CallTest extends TestCase $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown arguments "unknown1", "unknown2" for function "date(format, timestamp)".'); - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown1' => '', 'unknown2' => '']]); } @@ -73,19 +73,19 @@ class CallTest extends TestCase $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "case_sensitivity" could not be assigned for function "substr_compare(main_str, str, offset, length, case_sensitivity)" because it is mapped to an internal PHP function which cannot determine default value for optional argument "length".'); - $node = $this->createFunctionExpression('substr_compare'); + $node = $this->createFunctionExpression('substr_compare', 'substr_compare'); $this->getArguments($node, ['substr_compare', ['abcd', 'bc', 'offset' => 1, 'case_sensitivity' => true]]); } public function testResolveArgumentsOnlyNecessaryArgumentsForCustomFunction() { - $node = $this->createFunctionExpression('custom_function'); + $node = $this->createFunctionExpression('custom_function', [$this, 'customFunction']); $this->assertEquals(['arg1'], $this->getArguments($node, [[$this, 'customFunction'], ['arg1' => 'arg1']])); } public function testGetArgumentsForStaticMethod() { - $node = $this->createFunctionExpression('custom_static_function'); + $node = $this->createFunctionExpression('custom_static_function', __CLASS__.'::customStaticFunction'); $this->assertEquals(['arg1'], $this->getArguments($node, [__CLASS__.'::customStaticFunction', ['arg1' => 'arg1']])); } @@ -94,7 +94,7 @@ class CallTest extends TestCase $this->expectException(\LogicException::class); $this->expectExceptionMessage('The last parameter of "Twig\\Tests\\Node\\Expression\\CallTest::customFunctionWithArbitraryArguments" for function "foo" must be an array with default value, eg. "array $arg = []".'); - $node = $this->createFunctionExpression('foo', true); + $node = $this->createFunctionExpression('foo', [$this, 'customFunctionWithArbitraryArguments'], true); $this->getArguments($node, [[$this, 'customFunctionWithArbitraryArguments'], []]); } @@ -102,7 +102,7 @@ class CallTest extends TestCase { $this->expectException(\LogicException::class); $this->expectExceptionMessage('Callback for function "foo" is not callable in the current scope.'); - $node = $this->createFunctionExpression('foo', true); + $node = $this->createFunctionExpression('foo', '', true); $this->getArguments($node, ['', []]); } @@ -111,7 +111,7 @@ class CallTest extends TestCase $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = $this->createFunctionExpression('foo', true); + $node = $this->createFunctionExpression('foo', 'Twig\Tests\Node\Expression\custom_call_test_function', true); $this->getArguments($node, ['Twig\Tests\Node\Expression\custom_call_test_function', []]); } @@ -120,7 +120,7 @@ class CallTest extends TestCase $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\CallableTestClass\\:\\:__invoke" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = $this->createFunctionExpression('foo', true); + $node = $this->createFunctionExpression('foo', new CallableTestClass(), true); $this->getArguments($node, [new CallableTestClass(), []]); } @@ -144,9 +144,9 @@ class CallTest extends TestCase return $m->invokeArgs($call, $args); } - private function createFunctionExpression($name, $isVariadic = false): Node_Expression_Call + private function createFunctionExpression($name, $callable, $isVariadic = false): Node_Expression_Call { - return new Node_Expression_Call(new TwigFunction($name, null, ['is_variadic' => $isVariadic]), new Node([]), 0); + return new Node_Expression_Call(new TwigFunction($name, $callable, ['is_variadic' => $isVariadic]), new Node([]), 0); } }