diff --git a/CHANGELOG b/CHANGELOG index 34a9e54de..cac6c3920 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Add support for named arguments to the `block`, and `attribute` functions * Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments * Add a `CallableArgumentsExtractor` class * Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`; diff --git a/doc/functions/attribute.rst b/doc/functions/attribute.rst index e9d9a842e..832859de1 100644 --- a/doc/functions/attribute.rst +++ b/doc/functions/attribute.rst @@ -21,3 +21,10 @@ attribute: The resolution algorithm is the same as the one used for the ``.`` notation, except that the item can be any valid expression. + +Arguments +--------- + +* ``variable``: The variable +* ``attribute``: The attribute name +* ``arguments``: An array of arguments to pass to the call diff --git a/doc/functions/block.rst b/doc/functions/block.rst index 117e160f5..efc516eb1 100644 --- a/doc/functions/block.rst +++ b/doc/functions/block.rst @@ -32,6 +32,12 @@ current template: ... {% endif %} +Arguments +--------- + +* ``name``: The block name +* ``template``: The template where to look for the block + .. seealso:: :doc:`extends<../tags/extends>`, :doc:`parent<../functions/parent>` diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index c2477986b..f9031b9ca 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -33,6 +33,7 @@ use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Node; +use Twig\Util\CallableArgumentsExtractor; /** * Parses expressions. @@ -458,7 +459,6 @@ class ExpressionParser { switch ($name) { case 'parent': - $this->parseArguments(); if (!\count($this->parser->getBlockStack())) { throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext()); } @@ -467,21 +467,23 @@ class ExpressionParser throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext()); } + $this->parseArguments(true); + return new ParentExpression($this->parser->peekBlockStack(), $line); case 'block': - $args = $this->parseArguments(); - if (\count($args) < 1) { - throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext()); - } + $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->getNode('0'), \count($args) > 1 ? $args->getNode('1') : null, $line); + return new BlockReferenceExpression($args[0], $args[1] ?? null, $line); case 'attribute': - $args = $this->parseArguments(); - if (\count($args) < 2) { - throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext()); - } + $fakeNode = new Node(lineno: $line); + $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); + $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = []) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($this->parseArguments(true)); - return new GetAttrExpression($args->getNode('0'), $args->getNode('1'), \count($args) > 2 ? $args->getNode('2') : null, Template::ANY_CALL, $line); + 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); diff --git a/tests/Fixtures/functions/attribute.test b/tests/Fixtures/functions/attribute.test index 4499ad4bd..2f70b08a4 100644 --- a/tests/Fixtures/functions/attribute.test +++ b/tests/Fixtures/functions/attribute.test @@ -2,17 +2,21 @@ "attribute" function --TEMPLATE-- {{ attribute(obj, method) }} +{{ attribute(variable=obj, attribute=method) }} {{ attribute(array, item) }} {{ attribute(obj, "bar", ["a", "b"]) }} {{ attribute(obj, "bar", arguments) }} +{{ attribute(variable=obj, attribute="bar", arguments=arguments) }} {{ attribute(obj, method) is defined ? 'ok' : 'ko' }} {{ attribute(obj, nonmethod) is defined ? 'ok' : 'ko' }} --DATA-- return ['obj' => new Twig\Tests\TwigTestFoo(), 'method' => 'foo', 'array' => ['foo' => 'bar'], 'item' => 'foo', 'nonmethod' => 'xxx', 'arguments' => ['a', 'b']] --EXPECT-- foo +foo bar bar_a-b bar_a-b +bar_a-b ok ko diff --git a/tests/Fixtures/functions/attribute_with_wrong_args.test b/tests/Fixtures/functions/attribute_with_wrong_args.test new file mode 100644 index 000000000..6e8a17cf8 --- /dev/null +++ b/tests/Fixtures/functions/attribute_with_wrong_args.test @@ -0,0 +1,8 @@ +--TEST-- +"attribute" function +--TEMPLATE-- +{{ attribute(var=var, template="tpl") }} +--DATA-- +return ['var' => null] +--EXCEPTION-- +Twig\Error\SyntaxError: Value for argument "variable" is required for function "attribute" in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/block.test b/tests/Fixtures/functions/block.test index 1a4fd5492..bf8decd1e 100644 --- a/tests/Fixtures/functions/block.test +++ b/tests/Fixtures/functions/block.test @@ -5,8 +5,9 @@ {% block bar %}BAR{% endblock %} --TEMPLATE(base.twig)-- {% block foo %}{{ block('bar') }}{% endblock %} +{% block baz %}{{ block(name='bar') }}{% endblock %} {% block bar %}BAR_BASE{% endblock %} --DATA-- return [] --EXPECT-- -BARBAR +BARBARBAR diff --git a/tests/Fixtures/functions/block_with_template.test b/tests/Fixtures/functions/block_with_template.test index 37cb7a481..2e0d2916c 100644 --- a/tests/Fixtures/functions/block_with_template.test +++ b/tests/Fixtures/functions/block_with_template.test @@ -6,6 +6,10 @@ {{ block('foo', included_loaded_internal) }} {% set output = block('foo', 'included.twig') %} {{ output }} +{% set output = block(name='foo', template='included.twig') %} +{{ output }} +{% set output = block(template='included.twig', name='foo') %} +{{ output }} {% block foo %}NOT FOO{% endblock %} --TEMPLATE(included.twig)-- {% block foo %}FOO{% endblock %} @@ -19,4 +23,6 @@ FOO FOO FOO FOO +FOO +FOO NOT FOO diff --git a/tests/Fixtures/functions/block_without_name.test b/tests/Fixtures/functions/block_without_name.test index 236df9451..61896a8a2 100644 --- a/tests/Fixtures/functions/block_without_name.test +++ b/tests/Fixtures/functions/block_without_name.test @@ -9,4 +9,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\SyntaxError: The "block" function takes one argument (the block name) in "base.twig" at line 2. +Twig\Error\SyntaxError: Value for argument "name" is required for function "block" in "base.twig" at line 2.