Skip to content

Commit 3eda65e

Browse files
committed
feature #4391 Add support for named arguments on macro calls and dot operator arguments (fabpot)
This PR was merged into the 3.x branch. Discussion ---------- Add support for named arguments on macro calls and dot operator arguments Closes #929 Closes #3635 Commits ------- 223d36b Add support for named arguments on macro calls and dot operator arguments
2 parents 10a4c82 + 223d36b commit 3eda65e

14 files changed

+127
-71
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# 3.15.0 (2024-XX-XX)
22

3+
* Add named arguments support for the dot operator arguments (`foo.bar(some: arg)`)
4+
* Add named arguments support for macros
35
* Add a new `guard` tag that allows to test if some Twig callables are available at compilation time
46
* Allow arrow functions everywhere
57
* Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`)

doc/tags/macro.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,17 @@ tag:
5252
{% import "forms.twig" as forms %}
5353
5454
The above ``import`` call imports the ``forms.twig`` file (which can contain
55-
only macros, or a template and some macros), and import the macros as items of
56-
the ``forms`` local variable.
55+
only macros, or a template and some macros), and import the macros as
56+
attributes of the ``forms`` local variable.
5757

5858
The macros can then be called at will in the *current* template:
5959

6060
.. code-block:: html+twig
6161

6262
<p>{{ forms.input('username') }}</p>
6363
<p>{{ forms.input('password', null, 'password') }}</p>
64+
{# You can also use named arguments #}
65+
<p>{{ forms.input(name: 'password', type: 'password') }}</p>
6466

6567
Alternatively you can import names from the template into the current namespace
6668
via the ``from`` tag:
@@ -70,6 +72,7 @@ via the ``from`` tag:
7072
{% from 'forms.twig' import input as input_field, textarea %}
7173

7274
<p>{{ input_field('password', '', 'password') }}</p>
75+
<p>{{ input_field(name: 'password', type: 'password') }}</p>
7376
<p>{{ textarea('comment') }}</p>
7477

7578
.. caution::

doc/templates.rst

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,13 @@ built-in functions.
225225
Named Arguments
226226
---------------
227227

228-
Named arguments are supported in functions, filters, and tests.
228+
Named arguments are supported everywhere you can pass arguments: functions,
229+
filters, tests, macros, and dot operator arguments.
230+
231+
.. versionadded:: 3.15
232+
233+
Named arguments for macros and dot operator arguments were added in Twig
234+
3.15.
229235

230236
.. versionadded:: 3.12
231237

@@ -873,12 +879,15 @@ The following operators don't fit into any of the other categories:
873879
* ``.``, ``[]``: Gets an attribute of a variable.
874880

875881
The (``.``) operator abstracts getting an attribute of a variable (methods,
876-
properties or constants of a PHP object, or items of a PHP array):
882+
properties or constants of a PHP object, or items of a PHP array):
877883

878884
.. code-block:: twig
879885
880886
{{ user.name }}
881887
888+
Twig supports a specific syntax via the ``[]`` operator for accessing items
889+
on sequences and mappings, like in ``user['name']``:
890+
882891
After the ``.``, you can use any expression by wrapping it with parenthesis
883892
``()``.
884893

@@ -900,6 +909,22 @@ The following operators don't fit into any of the other categories:
900909
Before Twig 3.15, use the :doc:`attribute <functions/attribute>` function
901910
instead for the two previous use cases.
902911

912+
Twig supports a specific syntax via the ``[]`` operator for accessing items
913+
on sequences and mappings:
914+
915+
.. code-block:: twig
916+
917+
{{ user['name'] }}
918+
919+
When calling a method, you can pass arguments using the ``()`` operator:
920+
921+
.. code-block:: twig
922+
923+
{{ html.generate_input() }}
924+
{{ html.generate_input('pwd', 'password') }}
925+
{# or using named arguments #}
926+
{{ html.generate_input(name: 'pwd', type: 'password') }}
927+
903928
.. sidebar:: PHP Implementation
904929

905930
To resolve ``user.name`` to a PHP call, Twig uses the following algorithm
@@ -915,8 +940,8 @@ The following operators don't fit into any of the other categories:
915940
* if not, and if ``strict_variables`` is ``false``, return ``null``;
916941
* if not, throw an exception.
917942

918-
Twig supports a specific syntax via the ``[]`` operator for accessing items
919-
on sequences and mappings, like in ``user['name']``:
943+
To resolve ``user['name']`` to a PHP call, Twig uses the following algorithm
944+
at runtime:
920945

921946
* check if ``user`` is an array and ``name`` a valid element;
922947
* if not, and if ``strict_variables`` is ``false``, return ``null``;

src/ExpressionParser.php

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Twig\Node\Expression\GetAttrExpression;
2727
use Twig\Node\Expression\MethodCallExpression;
2828
use Twig\Node\Expression\NameExpression;
29+
use Twig\Node\Expression\TempNameExpression;
2930
use Twig\Node\Expression\TestExpression;
3031
use Twig\Node\Expression\Unary\AbstractUnary;
3132
use Twig\Node\Expression\Unary\NegUnary;
@@ -530,11 +531,7 @@ public function parsePostfixExpression($node)
530531
public function getFunctionNode($name, $line)
531532
{
532533
if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
533-
$arguments = new ArrayExpression([], $line);
534-
foreach ($this->parseArguments() as $n) {
535-
$arguments->addElement($n);
536-
}
537-
534+
$arguments = $this->createArguments($line);
538535
$node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
539536
$node->setAttribute('safe', true);
540537

@@ -575,9 +572,7 @@ public function parseSubscriptExpression($node)
575572
$stream->expect(Token::PUNCTUATION_TYPE, ')');
576573
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
577574
$type = Template::METHOD_CALL;
578-
foreach ($this->parseArguments() as $n) {
579-
$arguments->addElement($n);
580-
}
575+
$arguments = $this->createArguments($lineno);
581576
}
582577

583578
return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
@@ -594,9 +589,7 @@ public function parseSubscriptExpression($node)
594589

595590
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
596591
$type = Template::METHOD_CALL;
597-
foreach ($this->parseArguments() as $n) {
598-
$arguments->addElement($n);
599-
}
592+
$arguments = $this->createArguments($lineno);
600593
}
601594
} else {
602595
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
@@ -708,6 +701,9 @@ public function parseArguments($namedArguments = false, $definition = false)
708701
if (func_num_args() > 2) {
709702
trigger_deprecation('twig/twig', '3.15', 'Passing a third argument ($allowArrow) to "%s()" is deprecated.', __METHOD__);
710703
}
704+
if (!$namedArguments) {
705+
trigger_deprecation('twig/twig', '3.15', 'Passing "false" for the first argument ($namedArguments) to "%s()" is deprecated.', __METHOD__);
706+
}
711707

712708
$args = [];
713709
$stream = $this->parser->getStream();
@@ -949,4 +945,14 @@ private function setDeprecationCheck(bool $deprecationCheck): bool
949945

950946
return $current;
951947
}
948+
949+
private function createArguments(int $line): ArrayExpression
950+
{
951+
$arguments = new ArrayExpression([], $line);
952+
foreach ($this->parseArguments(true) as $k => $n) {
953+
$arguments->addElement($n, new TempNameExpression($k, $line));
954+
}
955+
956+
return $arguments;
957+
}
952958
}

src/Node/Expression/ArrayExpression.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,17 @@ public function compile(Compiler $compiler): void
9898
$compiler->raw('...')->subcompile($pair['value']);
9999
++$nextIndex;
100100
} else {
101-
$key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null;
101+
$key = null;
102102
if ($pair['key'] instanceof NameExpression) {
103103
$pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine());
104104
}
105+
if ($pair['key'] instanceof TempNameExpression) {
106+
$key = $pair['key']->getAttribute('name');
107+
$pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine());
108+
}
109+
if ($pair['key'] instanceof ConstantExpression) {
110+
$key = $pair['key']->getAttribute('value');
111+
}
105112

106113
if ($nextIndex !== $key) {
107114
if (\is_int($key)) {

src/Node/Expression/GetAttrExpression.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,21 @@
1818

1919
class GetAttrExpression extends AbstractExpression
2020
{
21+
22+
/**
23+
* @param ArrayExpression|NameExpression|null $arguments
24+
*/
2125
public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno)
2226
{
2327
$nodes = ['node' => $node, 'attribute' => $attribute];
2428
if (null !== $arguments) {
2529
$nodes['arguments'] = $arguments;
2630
}
2731

32+
if ($arguments && !$arguments instanceof ArrayExpression && !$arguments instanceof NameExpression) {
33+
trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class));
34+
}
35+
2836
parent::__construct($nodes, ['type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'optimizable' => true], $lineno);
2937
}
3038

src/Node/Expression/MethodCallExpression.php

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,9 @@ public function compile(Compiler $compiler): void
4343
->repr($this->getNode('node')->getAttribute('name'))
4444
->raw('], ')
4545
->repr($this->getAttribute('method'))
46-
->raw(', [')
47-
;
48-
$first = true;
49-
/** @var ArrayExpression */
50-
$args = $this->getNode('arguments');
51-
foreach ($args->getKeyValuePairs() as $pair) {
52-
if (!$first) {
53-
$compiler->raw(', ');
54-
}
55-
$first = false;
56-
57-
$compiler->subcompile($pair['value']);
58-
}
59-
$compiler
60-
->raw('], ')
46+
->raw(', ')
47+
->subcompile($this->getNode('arguments'))
48+
->raw(', ')
6149
->repr($this->getTemplateLine())
6250
->raw(', $context, $this->getSourceContext())');
6351
}

src/Node/Expression/TempNameExpression.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@
1515

1616
class TempNameExpression extends AbstractExpression
1717
{
18-
public function __construct(string $name, int $lineno)
18+
public const RESERVED_NAMES = ['varargs', 'context', 'macros', 'blocks', 'this'];
19+
20+
public function __construct(string|int $name, int $lineno)
1921
{
22+
if (is_int($name) || ctype_digit($name)) {
23+
$name = (int) $name;
24+
} elseif (in_array($name, self::RESERVED_NAMES)) {
25+
$name = '_'.$name.'_';
26+
}
27+
2028
parent::__construct([], ['name' => $name], $lineno);
2129
}
2230

2331
public function compile(Compiler $compiler): void
2432
{
25-
$compiler
26-
->raw('$_')
27-
->raw($this->getAttribute('name'))
28-
->raw('_')
29-
;
33+
$compiler->raw('$'.$this->getAttribute('name'));
3034
}
3135
}

src/Node/MacroNode.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Twig\Attribute\YieldReady;
1515
use Twig\Compiler;
1616
use Twig\Error\SyntaxError;
17+
use Twig\Node\Expression\TempNameExpression;
1718

1819
/**
1920
* Represents a macro node.
@@ -31,13 +32,17 @@ class MacroNode extends Node
3132
public function __construct(string $name, Node $body, Node $arguments, int $lineno)
3233
{
3334
if (!$body instanceof BodyNode) {
34-
trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class));
35+
trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated ("%s" given).', BodyNode::class, static::class, $body::class));
3536
}
3637

3738
foreach ($arguments as $argumentName => $argument) {
3839
if (self::VARARGS_NAME === $argumentName) {
3940
throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext());
4041
}
42+
if (in_array($argumentName, TempNameExpression::RESERVED_NAMES)) {
43+
$arguments->setNode('_'.$argumentName.'_', $argument);
44+
$arguments->removeNode($argumentName);
45+
}
4146
}
4247

4348
parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno);
@@ -54,7 +59,7 @@ public function compile(Compiler $compiler): void
5459
$pos = 0;
5560
foreach ($this->getNode('arguments') as $name => $default) {
5661
$compiler
57-
->raw('$__'.$name.'__ = ')
62+
->raw('$'.$name.' = ')
5863
->subcompile($default)
5964
;
6065

@@ -68,7 +73,7 @@ public function compile(Compiler $compiler): void
6873
}
6974

7075
$compiler
71-
->raw('...$__varargs__')
76+
->raw('...$varargs')
7277
->raw(")\n")
7378
->write("{\n")
7479
->indent()
@@ -80,8 +85,8 @@ public function compile(Compiler $compiler): void
8085
foreach ($this->getNode('arguments') as $name => $default) {
8186
$compiler
8287
->write('')
83-
->string($name)
84-
->raw(' => $__'.$name.'__')
88+
->string(trim($name, '_'))
89+
->raw(' => $'.$name)
8590
->raw(",\n")
8691
;
8792
}
@@ -92,7 +97,7 @@ public function compile(Compiler $compiler): void
9297
->write('')
9398
->string(self::VARARGS_NAME)
9499
->raw(' => ')
95-
->raw("\$__varargs__,\n")
100+
->raw("\$varargs,\n")
96101
->outdent()
97102
->write("] + \$this->env->getGlobals();\n\n")
98103
->write("\$blocks = [];\n\n")

tests/ExpressionParserTest.php

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -275,24 +275,6 @@ public static function getTestsForString()
275275
];
276276
}
277277

278-
public function testAttributeCallDoesNotSupportNamedArguments()
279-
{
280-
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
281-
$parser = new Parser($env);
282-
283-
$this->expectException(SyntaxError::class);
284-
$parser->parse($env->tokenize(new Source('{{ foo.bar(name="Foo") }}', 'index')));
285-
}
286-
287-
public function testMacroCallDoesNotSupportNamedArguments()
288-
{
289-
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
290-
$parser = new Parser($env);
291-
292-
$this->expectException(SyntaxError::class);
293-
$parser->parse($env->tokenize(new Source('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index')));
294-
}
295-
296278
public function testMacroDefinitionDoesNotSupportNonNameVariableName()
297279
{
298280
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);

0 commit comments

Comments
 (0)