From bfbbef05f286223e9bf8127e38e5be9d8b31d290 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 26 Jan 2025 21:04:54 +0100 Subject: [PATCH] Add the = assignment operator --- CHANGELOG | 1 + doc/operators_precedence.rst | 4 ++ doc/tags/set.rst | 10 ++++ doc/templates.rst | 52 +++++++++++++++++- src/ExpressionParser/Infix/ArgumentsTrait.php | 6 +- .../Infix/AssignmentExpressionParser.php | 55 +++++++++++++++++++ src/Extension/CoreExtension.php | 4 ++ src/Lexer.php | 2 +- src/Node/Expression/Binary/SetBinary.php | 44 +++++++++++++++ tests/ExpressionParserTest.php | 7 +++ .../syntax_error_in_reused_template.test | 4 +- tests/Fixtures/expressions/set.test | 20 +++++++ 12 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 src/ExpressionParser/Infix/AssignmentExpressionParser.php create mode 100644 src/Node/Expression/Binary/SetBinary.php create mode 100644 tests/Fixtures/expressions/set.test diff --git a/CHANGELOG b/CHANGELOG index c78ad440d..db9d61e41 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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) diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst index 2d445564e..8e8ec343f 100644 --- a/doc/operators_precedence.rst +++ b/doc/operators_precedence.rst @@ -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 | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ diff --git a/doc/tags/set.rst b/doc/tags/set.rst index 0ebf7ad5d..e87640fa4 100644 --- a/doc/tags/set.rst +++ b/doc/tags/set.rst @@ -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 + `: + + .. 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 diff --git a/doc/templates.rst b/doc/templates.rst index af0a59b33..2f869c74c 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -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` tag: +You can assign values to variables inside code blocks using either the +:doc:`set` tag or the :ref:`= 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 %} + + {% endset %} + +See the :doc:`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 diff --git a/src/ExpressionParser/Infix/ArgumentsTrait.php b/src/ExpressionParser/Infix/ArgumentsTrait.php index 1c2ae49dd..96bde555d 100644 --- a/src/ExpressionParser/Infix/ArgumentsTrait.php +++ b/src/ExpressionParser/Infix/ArgumentsTrait.php @@ -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()); } diff --git a/src/ExpressionParser/Infix/AssignmentExpressionParser.php b/src/ExpressionParser/Infix/AssignmentExpressionParser.php new file mode 100644 index 000000000..f5965613f --- /dev/null +++ b/src/ExpressionParser/Infix/AssignmentExpressionParser.php @@ -0,0 +1,55 @@ +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'; + } +} diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 061cd5684..345f40666 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -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(), diff --git a/src/Lexer.php b/src/Lexer.php index 027771acc..508a79c02 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -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()); } diff --git a/src/Node/Expression/Binary/SetBinary.php b/src/Node/Expression/Binary/SetBinary.php new file mode 100644 index 000000000..7b3443298 --- /dev/null +++ b/src/Node/Expression/Binary/SetBinary.php @@ -0,0 +1,44 @@ + + */ +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('='); + } +} diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 5da06b68c..d7728b966 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -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;')]; } } diff --git a/tests/Fixtures/exceptions/syntax_error_in_reused_template.test b/tests/Fixtures/exceptions/syntax_error_in_reused_template.test index 4ce06854f..0ee19cc62 100644 --- a/tests/Fixtures/exceptions/syntax_error_in_reused_template.test +++ b/tests/Fixtures/exceptions/syntax_error_in_reused_template.test @@ -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. diff --git a/tests/Fixtures/expressions/set.test b/tests/Fixtures/expressions/set.test new file mode 100644 index 000000000..00b8d28e3 --- /dev/null +++ b/tests/Fixtures/expressions/set.test @@ -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