diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 535c459a7..7477d0bec 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -457,60 +457,37 @@ class ExpressionParser public function getFunctionNode($name, $line) { - switch ($name) { - case 'parent': - if (!\count($this->parser->getBlockStack())) { - throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext()); - } + if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { + $arguments = new ArrayExpression([], $line); + foreach ($this->parseArguments() as $n) { + $arguments->addElement($n); + } - if (!$this->parser->getParent() && !$this->parser->hasTraits()) { - throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext()); - } + $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line); + $node->setAttribute('safe', true); - $this->parseArguments(true); - - return new ParentExpression($this->parser->peekBlockStack(), $line); - case 'block': - $fakeNode = new Node(lineno: $line); - $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); - $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null); - $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($this->parseArguments(true)); - - return new BlockReferenceExpression($args[0], $args[1] ?? null, $line); - case 'attribute': - $fakeNode = new Node(lineno: $line); - $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); - $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); - $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($this->parseArguments(true)); - - return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); - default: - if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { - $arguments = new ArrayExpression([], $line); - foreach ($this->parseArguments() as $n) { - $arguments->addElement($n); - } - - $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line); - $node->setAttribute('safe', true); - - return $node; - } - - $args = $this->parseArguments(true); - $function = $this->getFunction($name, $line); - - $ready = true; - if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { - $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); - } - - if (!$ready = $this->readyNodes[$class]) { - trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); - } - - return new $class($ready ? $function : $function->getName(), $args, $line); + return $node; } + + $args = $this->parseArguments(true); + $function = $this->getFunction($name, $line); + + if ($function->getParserCallable()) { + $fakeNode = new Node(lineno: $line); + $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); + + return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line); + } + + if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($ready ? $function : $function->getName(), $args, $line); } public function parseSubscriptExpression($node) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 5f3b753c1..1efb7ecea 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -14,8 +14,10 @@ namespace Twig\Extension; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; use Twig\ExpressionParser; use Twig\Markup; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\AndBinary; use Twig\Node\Expression\Binary\BitwiseAndBinary; @@ -44,9 +46,12 @@ use Twig\Node\Expression\Binary\RangeBinary; use Twig\Node\Expression\Binary\SpaceshipBinary; use Twig\Node\Expression\Binary\StartsWithBinary; use Twig\Node\Expression\Binary\SubBinary; +use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; use Twig\Node\Expression\FunctionNode\EnumCasesFunction; +use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NullCoalesceExpression; +use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\Test\ConstantTest; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\DivisiblebyTest; @@ -57,7 +62,9 @@ use Twig\Node\Expression\Test\SameasTest; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; +use Twig\Node\Node; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\Parser; use Twig\Source; use Twig\Template; use Twig\TemplateWrapper; @@ -80,6 +87,7 @@ use Twig\TokenParser\WithTokenParser; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; +use Twig\Util\CallableArgumentsExtractor; final class CoreExtension extends AbstractExtension { @@ -238,6 +246,9 @@ final class CoreExtension extends AbstractExtension public function getFunctions(): array { return [ + new TwigFunction('parent', null, ['parser_callable' => [$this, 'parseParentFunction']]), + new TwigFunction('block', null, ['parser_callable' => [$this, 'parseBlockFunction']]), + new TwigFunction('attribute', null, ['parser_callable' => [$this, 'parseAttributeFunction']]), new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), new TwigFunction('range', 'range'), @@ -1905,4 +1916,42 @@ final class CoreExtension extends AbstractExtension return $output; } + + /** + * @internal + */ + public function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + if (!\count($parser->getBlockStack())) { + throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $parser->getStream()->getSourceContext()); + } + + if (!$parser->getParent() && !$parser->hasTraits()) { + throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $parser->getStream()->getSourceContext()); + } + + return new ParentExpression($parser->peekBlockStack(), $line); + } + + /** + * @internal + */ + public function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + + return new BlockReferenceExpression($args[0], $args[1] ?? null, $line); + } + + /** + * @internal + */ + public function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + + return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); + } } diff --git a/src/TwigFunction.php b/src/TwigFunction.php index 4797320eb..d90a7e42c 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -34,9 +34,15 @@ final class TwigFunction extends AbstractTwigCallable 'is_safe' => null, 'is_safe_callback' => null, 'node_class' => FunctionExpression::class, + 'parser_callable' => null, ], $this->options); } + public function getParserCallable(): ?callable + { + return $this->options['parser_callable']; + } + public function getSafe(Node $functionArgs): ?array { if (null !== $this->options['is_safe']) {