feature #4735 Add the = assignment operator (fabpot)

This PR was merged into the 3.x branch.

Discussion
----------

Add the = assignment operator

Commits
-------

bfbbef05f2 Add the = assignment operator
This commit is contained in:
Fabien Potencier
2026-01-19 21:23:20 +01:00
12 changed files with 203 additions and 6 deletions
+1
View File
@@ -1,5 +1,6 @@
# 3.23.0 (2026-XX-XX)
* Add `=` assignment operator (allows to set variables in expression or to replace the short-form of the set tag)
* Add `?.` null-safe operator
* Add `===` and `!==` operators (equivalent to the `same as` and `not same as` tests)
+4
View File
@@ -96,6 +96,8 @@
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``?`` | infix | Left | Conditional operator (a ? b : c) |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``=`` | | Right | Assignment operator |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
When a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``.
@@ -198,3 +200,5 @@ Here is the same table for Twig 4.0 with adjusted precedences:
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``?`` | infix | Left | Conditional operator (a ? b : c) |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``=`` | | Right | Assignment operator |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
+10
View File
@@ -27,6 +27,16 @@ The assigned value can be any valid :ref:`Twig expression
{% set user = {'name': 'Fabien'} %}
{% set name = 'Fabien' ~ ' ' ~ 'Potencier' %}
.. tip::
To assign a value within an expression, use the :ref:`= operator
<assignment-operator>`:
.. code-block:: twig
{# use assignment within a larger expression #}
{{ (result = fetch_data()) ? result : 'default' }}
Several variables can be assigned in one block:
.. code-block:: twig
+50 -2
View File
@@ -131,8 +131,8 @@ The following variables are always available in templates:
Setting Variables
~~~~~~~~~~~~~~~~~
You can assign values to variables inside code blocks. Assignments use the
:doc:`set<tags/set>` tag:
You can assign values to variables inside code blocks using either the
:doc:`set<tags/set>` tag or the :ref:`= operator <assignment-operator>`:
.. code-block:: twig
@@ -140,6 +140,31 @@ You can assign values to variables inside code blocks. Assignments use the
{% set numbers = [1, 2] %}
{% set map = {'city': 'Paris'} %}
{# or #}
{% do name = 'Fabien' %}
{% do numbers = [1, 2] %}
{% do map = {'city': 'Paris'} %}
For simple assignments, both are equivalent. However, the ``set`` tag provides
additional features:
* **Multi-target assignment**: Assign multiple variables at once:
.. code-block:: twig
{% set first, last = 'Fabien', 'Potencier' %}
* **Block capture**: Capture chunks of template content into a variable:
.. code-block:: html+twig
{% set content %}
<div id="pagination">...</div>
{% endset %}
See the :doc:`set<tags/set>` tag documentation for more details.
Filters
-------
@@ -994,6 +1019,29 @@ The following operators don't fit into any of the other categories:
Support for expanding the arguments of a function call was introduced in
Twig 3.15.
.. _assignment-operator:
* ``=``: The assignment operator assigns a value to a variable within an
expression:
.. code-block:: twig
{# assign #}
{% do b = 1 + 3 %}
{# assign and output the result #}
{{ b = 1 + 3 }}
{# assignments can be chained #}
{% do a = b = 'foo' %}
{# assignment can be used inside other expressions #}
{% do a = (b = 4) + 5 %}
.. versionadded:: 3.23
The ``=`` assignment operator was added in Twig 3.23.
* ``=>``: The arrow operator allows the creation of functions. A function is
made of arguments (use parentheses for multiple arguments) and an arrow
(``=>``) followed by an expression to execute. The expression has access to
@@ -13,6 +13,7 @@ namespace Twig\ExpressionParser\Infix;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\SetBinary;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Expression\Variable\LocalVariable;
@@ -58,7 +59,10 @@ trait ArgumentsTrait
}
$name = null;
if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) {
if ($value instanceof SetBinary) {
$name = $value->getNode('left')->getAttribute('name');
$value = $value->getNode('right');
} elseif (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) {
if (!$value instanceof ContextVariable) {
throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext());
}
@@ -0,0 +1,55 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\ExpressionParser\Infix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\SetBinary;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
class AssignmentExpressionParser extends BinaryOperatorExpressionParser
{
public function __construct(
string $name,
) {
parent::__construct(SetBinary::class, $name, 0, InfixAssociativity::Right);
}
/**
* @return AbstractBinary
*/
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
{
if (!$left instanceof ContextVariable) {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned.', $left::class), $token->getLine(), $parser->getStream()->getSourceContext());
}
$right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence());
$right = match ($this->getName()) {
'=' => $right,
default => throw new \LogicException(\sprintf('Unknown operator: %s.', $this->getName())),
};
return new SetBinary($left, $right, $token->getLine());
}
public function getDescription(): string
{
return 'Assignment operator';
}
}
+4
View File
@@ -17,6 +17,7 @@ use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\Infix\ArrowExpressionParser;
use Twig\ExpressionParser\Infix\AssignmentExpressionParser;
use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
use Twig\ExpressionParser\Infix\ConditionalTernaryExpressionParser;
use Twig\ExpressionParser\Infix\DotExpressionParser;
@@ -377,6 +378,9 @@ final class CoreExtension extends AbstractExtension
// ternary operator
new ConditionalTernaryExpressionParser(),
// assignment operator
new AssignmentExpressionParser('='),
// Twig callables
new IsExpressionParser(),
new IsNotExpressionParser(),
+1 -1
View File
@@ -525,7 +525,7 @@ class Lexer
private function getOperatorRegex(): string
{
$expressionParsers = ['='];
$expressionParsers = [];
foreach ($this->env->getExpressionParsers() as $expressionParser) {
$expressionParsers = array_merge($expressionParsers, [$expressionParser->getName()], $expressionParser->getAliases());
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class SetBinary extends AbstractBinary
{
/**
* @param ContextVariable $left
* @param AbstractExpression $right
*/
public function __construct(Node $left, Node $right, int $lineno)
{
$name = $left->getAttribute('name');
if (!\is_string($name)) {
throw new \LogicException('The "name" attribute must be a string.');
}
$left = new AssignContextVariable($name, $left->getTemplateLine());
parent::__construct($left, $right, $lineno);
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('=');
}
}
+7
View File
@@ -735,6 +735,13 @@ class ExpressionParserTest extends TestCase
// ?? stronger than ()
// yield '?? vs ()' => ['{{ (1 ?? "a") }}', '{{ ((1 ?? "a")) }}', eval("return 1;")];
// = stronger than anything else
yield '= same as literal' => ['{% do c = "a" %}{{ c }}', '{% do c = ("a") %}{{ c }}', eval("return 'a';")];
yield '= stronger than .' => ['{% do c = a.b %}{{ c }}', '{% do c = (a.b) %}{{ c }}', eval("\$a = ['b' => 1]; return \$a['b'];"), $context];
yield '= stronger than math' => ['{% do a = 1 + 3 %}{{ a }}', '{% do a = (1 + 3) %}{{ a }}', eval('$a = 1 + 3; return $a;')];
yield '= stronger than logical' => ['{% do a = false or true %}{{ a }}', '{% do a = (false or true) %}{{ a }}', eval('$a = false || true; return $a;')];
yield '= stronger than ternary' => ['{% do c = 4 ? 0 : -1 %}{{ c }}', '{% do c = (4 ? 0 : -1) %}{{ c }}', eval('return 4 ? 0 : -1;')];
}
}
@@ -4,7 +4,7 @@ Exception for syntax error in reused template
{% use 'foo.twig' %}
--TEMPLATE(foo.twig)--
{% block bar %}
{% do node.data = 5 %}
{% do node.data 5 %}
{% endblock %}
--EXCEPTION--
Twig\Error\SyntaxError: Unexpected token "operator" of value "=" ("end of statement block" expected) in "foo.twig" at line 3.
Twig\Error\SyntaxError: Unexpected token "number" of value "5" ("end of statement block" expected) in "foo.twig" at line 3.
+20
View File
@@ -0,0 +1,20 @@
--TEST--
Twig supports the "=" operator (assignment)
--TEMPLATE--
{# stores #}
{% do b = 1 + 3 %}{{ b }}
{# stores and displays #}
{{ b = 1 + 3 }}{{ b }}
{# stores and returns #}
{% do (c = 4) ? 0 : -1 %}{{ c }}
{# = can be chained #}
{% do c = d = "a" %}{{ c }}{{ d }}
{% do a = (b = 4) + 5 %}{{ a }}{{ b }}
--DATA--
return []
--EXPECT--
4
44
4
aa
94