Skip to content

Commit be1797d

Browse files
committed
feature #4735 Add the = assignment operator (fabpot)
This PR was merged into the 3.x branch. Discussion ---------- Add the = assignment operator Commits ------- bfbbef0 Add the = assignment operator
2 parents 820eed3 + bfbbef0 commit be1797d

12 files changed

Lines changed: 203 additions & 6 deletions

File tree

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# 3.23.0 (2026-XX-XX)
22

3+
* Add `=` assignment operator (allows to set variables in expression or to replace the short-form of the set tag)
34
* Add `?.` null-safe operator
45
* Add `===` and `!==` operators (equivalent to the `same as` and `not same as` tests)
56

doc/operators_precedence.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@
9696
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
9797
| | ``?`` | infix | Left | Conditional operator (a ? b : c) |
9898
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
99+
| | ``=`` | | Right | Assignment operator |
100+
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
99101

100102
When a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``.
101103

@@ -198,3 +200,5 @@ Here is the same table for Twig 4.0 with adjusted precedences:
198200
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
199201
| | ``?`` | infix | Left | Conditional operator (a ? b : c) |
200202
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
203+
| | ``=`` | | Right | Assignment operator |
204+
+------------+------------------+---------+---------------+-------------------------------------------------------------------+

doc/tags/set.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ The assigned value can be any valid :ref:`Twig expression
2727
{% set user = {'name': 'Fabien'} %}
2828
{% set name = 'Fabien' ~ ' ' ~ 'Potencier' %}
2929
30+
.. tip::
31+
32+
To assign a value within an expression, use the :ref:`= operator
33+
<assignment-operator>`:
34+
35+
.. code-block:: twig
36+
37+
{# use assignment within a larger expression #}
38+
{{ (result = fetch_data()) ? result : 'default' }}
39+
3040
Several variables can be assigned in one block:
3141

3242
.. code-block:: twig

doc/templates.rst

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,40 @@ The following variables are always available in templates:
131131
Setting Variables
132132
~~~~~~~~~~~~~~~~~
133133

134-
You can assign values to variables inside code blocks. Assignments use the
135-
:doc:`set<tags/set>` tag:
134+
You can assign values to variables inside code blocks using either the
135+
:doc:`set<tags/set>` tag or the :ref:`= operator <assignment-operator>`:
136136

137137
.. code-block:: twig
138138
139139
{% set name = 'Fabien' %}
140140
{% set numbers = [1, 2] %}
141141
{% set map = {'city': 'Paris'} %}
142142
143+
{# or #}
144+
145+
{% do name = 'Fabien' %}
146+
{% do numbers = [1, 2] %}
147+
{% do map = {'city': 'Paris'} %}
148+
149+
For simple assignments, both are equivalent. However, the ``set`` tag provides
150+
additional features:
151+
152+
* **Multi-target assignment**: Assign multiple variables at once:
153+
154+
.. code-block:: twig
155+
156+
{% set first, last = 'Fabien', 'Potencier' %}
157+
158+
* **Block capture**: Capture chunks of template content into a variable:
159+
160+
.. code-block:: html+twig
161+
162+
{% set content %}
163+
<div id="pagination">...</div>
164+
{% endset %}
165+
166+
See the :doc:`set<tags/set>` tag documentation for more details.
167+
143168
Filters
144169
-------
145170

@@ -994,6 +1019,29 @@ The following operators don't fit into any of the other categories:
9941019
Support for expanding the arguments of a function call was introduced in
9951020
Twig 3.15.
9961021

1022+
.. _assignment-operator:
1023+
1024+
* ``=``: The assignment operator assigns a value to a variable within an
1025+
expression:
1026+
1027+
.. code-block:: twig
1028+
1029+
{# assign #}
1030+
{% do b = 1 + 3 %}
1031+
1032+
{# assign and output the result #}
1033+
{{ b = 1 + 3 }}
1034+
1035+
{# assignments can be chained #}
1036+
{% do a = b = 'foo' %}
1037+
1038+
{# assignment can be used inside other expressions #}
1039+
{% do a = (b = 4) + 5 %}
1040+
1041+
.. versionadded:: 3.23
1042+
1043+
The ``=`` assignment operator was added in Twig 3.23.
1044+
9971045
* ``=>``: The arrow operator allows the creation of functions. A function is
9981046
made of arguments (use parentheses for multiple arguments) and an arrow
9991047
(``=>``) followed by an expression to execute. The expression has access to

src/ExpressionParser/Infix/ArgumentsTrait.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Twig\Error\SyntaxError;
1515
use Twig\Node\Expression\ArrayExpression;
16+
use Twig\Node\Expression\Binary\SetBinary;
1617
use Twig\Node\Expression\Unary\SpreadUnary;
1718
use Twig\Node\Expression\Variable\ContextVariable;
1819
use Twig\Node\Expression\Variable\LocalVariable;
@@ -58,7 +59,10 @@ private function parseNamedArguments(Parser $parser, bool $parseOpenParenthesis
5859
}
5960

6061
$name = null;
61-
if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) {
62+
if ($value instanceof SetBinary) {
63+
$name = $value->getNode('left')->getAttribute('name');
64+
$value = $value->getNode('right');
65+
} elseif (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) {
6266
if (!$value instanceof ContextVariable) {
6367
throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext());
6468
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Twig\ExpressionParser\Infix;
13+
14+
use Twig\Error\SyntaxError;
15+
use Twig\ExpressionParser\InfixAssociativity;
16+
use Twig\Node\Expression\AbstractExpression;
17+
use Twig\Node\Expression\Binary\AbstractBinary;
18+
use Twig\Node\Expression\Binary\SetBinary;
19+
use Twig\Node\Expression\Variable\ContextVariable;
20+
use Twig\Parser;
21+
use Twig\Token;
22+
23+
/**
24+
* @internal
25+
*/
26+
class AssignmentExpressionParser extends BinaryOperatorExpressionParser
27+
{
28+
public function __construct(
29+
string $name,
30+
) {
31+
parent::__construct(SetBinary::class, $name, 0, InfixAssociativity::Right);
32+
}
33+
34+
/**
35+
* @return AbstractBinary
36+
*/
37+
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
38+
{
39+
if (!$left instanceof ContextVariable) {
40+
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned.', $left::class), $token->getLine(), $parser->getStream()->getSourceContext());
41+
}
42+
$right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence());
43+
$right = match ($this->getName()) {
44+
'=' => $right,
45+
default => throw new \LogicException(\sprintf('Unknown operator: %s.', $this->getName())),
46+
};
47+
48+
return new SetBinary($left, $right, $token->getLine());
49+
}
50+
51+
public function getDescription(): string
52+
{
53+
return 'Assignment operator';
54+
}
55+
}

src/Extension/CoreExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Twig\Error\RuntimeError;
1818
use Twig\Error\SyntaxError;
1919
use Twig\ExpressionParser\Infix\ArrowExpressionParser;
20+
use Twig\ExpressionParser\Infix\AssignmentExpressionParser;
2021
use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
2122
use Twig\ExpressionParser\Infix\ConditionalTernaryExpressionParser;
2223
use Twig\ExpressionParser\Infix\DotExpressionParser;
@@ -377,6 +378,9 @@ public function getExpressionParsers(): array
377378
// ternary operator
378379
new ConditionalTernaryExpressionParser(),
379380

381+
// assignment operator
382+
new AssignmentExpressionParser('='),
383+
380384
// Twig callables
381385
new IsExpressionParser(),
382386
new IsNotExpressionParser(),

src/Lexer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ private function moveCursor($text): void
525525

526526
private function getOperatorRegex(): string
527527
{
528-
$expressionParsers = ['='];
528+
$expressionParsers = [];
529529
foreach ($this->env->getExpressionParsers() as $expressionParser) {
530530
$expressionParsers = array_merge($expressionParsers, [$expressionParser->getName()], $expressionParser->getAliases());
531531
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Twig\Node\Expression\Binary;
13+
14+
use Twig\Compiler;
15+
use Twig\Node\Expression\AbstractExpression;
16+
use Twig\Node\Expression\Variable\AssignContextVariable;
17+
use Twig\Node\Expression\Variable\ContextVariable;
18+
use Twig\Node\Node;
19+
20+
/**
21+
* @author Fabien Potencier <fabien@symfony.com>
22+
*/
23+
class SetBinary extends AbstractBinary
24+
{
25+
/**
26+
* @param ContextVariable $left
27+
* @param AbstractExpression $right
28+
*/
29+
public function __construct(Node $left, Node $right, int $lineno)
30+
{
31+
$name = $left->getAttribute('name');
32+
if (!\is_string($name)) {
33+
throw new \LogicException('The "name" attribute must be a string.');
34+
}
35+
$left = new AssignContextVariable($name, $left->getTemplateLine());
36+
37+
parent::__construct($left, $right, $lineno);
38+
}
39+
40+
public function operator(Compiler $compiler): Compiler
41+
{
42+
return $compiler->raw('=');
43+
}
44+
}

tests/ExpressionParserTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,13 @@ public static function getBindingPowerTests(): iterable
735735

736736
// ?? stronger than ()
737737
// yield '?? vs ()' => ['{{ (1 ?? "a") }}', '{{ ((1 ?? "a")) }}', eval("return 1;")];
738+
739+
// = stronger than anything else
740+
yield '= same as literal' => ['{% do c = "a" %}{{ c }}', '{% do c = ("a") %}{{ c }}', eval("return 'a';")];
741+
yield '= stronger than .' => ['{% do c = a.b %}{{ c }}', '{% do c = (a.b) %}{{ c }}', eval("\$a = ['b' => 1]; return \$a['b'];"), $context];
742+
yield '= stronger than math' => ['{% do a = 1 + 3 %}{{ a }}', '{% do a = (1 + 3) %}{{ a }}', eval('$a = 1 + 3; return $a;')];
743+
yield '= stronger than logical' => ['{% do a = false or true %}{{ a }}', '{% do a = (false or true) %}{{ a }}', eval('$a = false || true; return $a;')];
744+
yield '= stronger than ternary' => ['{% do c = 4 ? 0 : -1 %}{{ c }}', '{% do c = (4 ? 0 : -1) %}{{ c }}', eval('return 4 ? 0 : -1;')];
738745
}
739746
}
740747

0 commit comments

Comments
 (0)