mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-02 21:46:48 +00:00
feature #4210 Special functions extension point (fabpot)
This PR was merged into the 3.x branch.
Discussion
----------
Special functions extension point
The `parent`, `attribute`, `block` (and `loop` in 4.0) functions are handled directly in ExpressionParser as they are converted to specific Nodes.
It means that recursive loops can only be implemented in core. By adding a new extension point, we are removing the hardcoded handling of such functions and we open up the possibility for user-land functions to do the same.
*Review is easier when hiding whitespace.*
Commits
-------
44280790 Add an extension point for parsing special functions
This commit is contained in:
+28
-51
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
Reference in New Issue
Block a user