Assignment operator array destructuring

This commit is contained in:
Fabien Potencier
2026-01-18 22:12:05 +01:00
parent dfeb162464
commit bb99af3b39
10 changed files with 195 additions and 16 deletions
+1
View File
@@ -1,6 +1,7 @@
# 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 array destructuring
* Add `?.` null-safe operator
* Add `===` and `!==` operators (equivalent to the `same as` and `not same as` tests)
+1 -1
View File
@@ -30,7 +30,7 @@ The assigned value can be any valid :ref:`Twig expression
.. tip::
To assign a value within an expression, use the :ref:`= operator
<assignment-operator>`:
<templates-assignment-operator>`:
.. code-block:: twig
+49 -12
View File
@@ -132,30 +132,24 @@ Setting Variables
~~~~~~~~~~~~~~~~~
You can assign values to variables inside code blocks using either the
:doc:`set<tags/set>` tag or the :ref:`= operator <assignment-operator>`:
:doc:`set<tags/set>` tag or the :ref:`= operator <templates-assignment-operator>`:
.. code-block:: twig
{% set name = 'Fabien' %}
{% set numbers = [1, 2] %}
{% set map = {'city': 'Paris'} %}
{% set first, last = 'Fabien', 'Potencier' %}
{# or #}
{% do name = 'Fabien' %}
{% do numbers = [1, 2] %}
{% do map = {'city': 'Paris'} %}
{% do [first, last] = ['Fabien', 'Potencier'] %}
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:
The ``set`` tag can also be used to capture template content into
a variable:
.. code-block:: html+twig
@@ -1019,7 +1013,7 @@ 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:
.. _templates-assignment-operator:
* ``=``: The assignment operator assigns a value to a variable within an
expression:
@@ -1038,6 +1032,9 @@ The following operators don't fit into any of the other categories:
{# assignment can be used inside other expressions #}
{% do a = (b = 4) + 5 %}
The assignment operator also supports :ref:`destructuring
<templates-destructuring>`.
.. versionadded:: 3.23
The ``=`` assignment operator was added in Twig 3.23.
@@ -1109,6 +1106,46 @@ parentheses:
{# use parenthesis to change precedence #}
{{ (greeting ~ name)|lower }} {# hello fabien #}
.. _templates-destructuring:
Destructuring
-------------
Destructuring allows you to extract values from arrays and assign them to
variables in a single operation using the ``=`` :ref:`assignment operator
<templates-assignment-operator>`.
.. versionadded:: 3.23
Destructuring was added in Twig 3.23.
Array Destructuring
~~~~~~~~~~~~~~~~~~~
Use square brackets on the left side of an assignment to destructure an array:
.. code-block:: twig
{% do [first, last] = ['Fabien', 'Potencier'] %}
{{ first }} {# Fabien #}
{{ last }} {# Potencier #}
If there are more variables than values, the extra variables are set to
``null``:
.. code-block:: twig
{# extra will be null #}
{% do [first, last, extra] = ['Fabien', 'Potencier'] %}
You can skip values by leaving a slot empty:
.. code-block:: twig
{# only assign the second value #}
{% do [, last] = ['Fabien', 'Potencier'] %}
.. _templates-whitespace-control:
Whitespace Control
@@ -14,7 +14,9 @@ namespace Twig\ExpressionParser\Infix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\DestructuringSetBinary;
use Twig\Node\Expression\Binary\SetBinary;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
@@ -36,7 +38,7 @@ class AssignmentExpressionParser extends BinaryOperatorExpressionParser
*/
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
{
if (!$left instanceof ContextVariable) {
if (!$left instanceof ContextVariable && !$left instanceof ArrayExpression) {
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());
@@ -45,7 +47,11 @@ class AssignmentExpressionParser extends BinaryOperatorExpressionParser
default => throw new \LogicException(\sprintf('Unknown operator: %s.', $this->getName())),
};
return new SetBinary($left, $right, $token->getLine());
if ($left instanceof ArrayExpression) {
return new DestructuringSetBinary($left, $right, $token->getLine());
} else {
return new SetBinary($left, $right, $token->getLine());
}
}
public function getDescription(): string
@@ -20,6 +20,7 @@ use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\EmptyExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
use Twig\Token;
@@ -170,7 +171,12 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
}
$first = false;
$node->addElement($parser->parseExpression());
// Check for empty slots (comma with no expression)
if ($stream->test(Token::PUNCTUATION_TYPE, ',')) {
$node->addElement(new EmptyExpression($stream->getCurrent()->getLine()));
} else {
$node->addElement($parser->parseExpression());
}
}
$stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');
+8
View File
@@ -12,6 +12,7 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\Expression\Unary\StringCastUnary;
use Twig\Node\Expression\Variable\ContextVariable;
@@ -77,6 +78,13 @@ class ArrayExpression extends AbstractExpression implements SupportDefinedTestIn
return;
}
// Check for empty expressions which are only allowed in destructuring
foreach ($this->getKeyValuePairs() as $pair) {
if ($pair['value'] instanceof EmptyExpression) {
throw new SyntaxError('Empty array elements are only allowed in destructuring assignments.', $pair['value']->getTemplateLine(), $this->getSourceContext());
}
}
$compiler->raw('[');
$isSequence = true;
foreach ($this->getKeyValuePairs() as $i => $pair) {
@@ -0,0 +1,67 @@
<?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\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\EmptyExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
/**
* @internal
*/
class DestructuringSetBinary extends AbstractBinary
{
private array $variables = [];
/**
* @param ArrayExpression $left The array expression containing variables to assign to
* @param AbstractExpression $right The expression providing values for assignment
*/
public function __construct(Node $left, Node $right, int $lineno)
{
foreach ($left->getKeyValuePairs() as $pair) {
if ($pair['value'] instanceof EmptyExpression) {
$this->variables[] = null;
} elseif ($pair['value'] instanceof ContextVariable) {
$this->variables[] = $pair['value']->getAttribute('name');
} else {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in destructuring.', $pair['value']::class), $lineno);
}
}
parent::__construct($left, $right, $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);
$compiler->raw('[');
foreach ($this->variables as $i => $name) {
if ($i) {
$compiler->raw(', ');
}
if (null !== $name) {
$compiler->raw('$context[')->repr($name)->raw(']');
}
}
$compiler->raw('] = array_pad(')->subcompile($this->getNode('right'))->raw(', ')->repr(\count($this->variables))->raw(', null)');
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('=');
}
}
+33
View File
@@ -0,0 +1,33 @@
<?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;
use Twig\Compiler;
/**
* Represents an empty slot in an array.
*
* This is currently only used in destructuring contexts.
*
* @internal
*/
final class EmptyExpression extends AbstractExpression
{
public function __construct(int $lineno)
{
parent::__construct([], [], $lineno);
}
public function compile(Compiler $compiler): void
{
}
}
+9
View File
@@ -231,6 +231,15 @@ class ExpressionParserTest extends TestCase
$parser->parse($stream);
}
public function testSequenceCompilationError()
{
$env = new Environment(new ArrayLoader(['index' => '{{ [1,,2] }}']), ['cache' => false, 'autoescape' => false]);
$this->expectException(SyntaxError::class);
$this->expectExceptionMessage('Empty array elements are only allowed in destructuring assignments');
$env->compileSource(new Source('{{ [1,,2] }}', 'index'));
}
/**
* @dataProvider getTestsForString
*/
+12
View File
@@ -10,6 +10,12 @@ Twig supports the "=" operator (assignment)
{# = can be chained #}
{% do c = d = "a" %}{{ c }}{{ d }}
{% do a = (b = 4) + 5 %}{{ a }}{{ b }}
# Array destructuring
{% do [first, last] = ['Fabien', 'Potencier'] %}{{ first }} {{ last }}
{% do [a, b] = [b, a] %}{{ a }}{{ b }}
{% do [, second] = ['first', 'second'] %}{{ second }}
{% do [x, y, z] = ['one', 'two'] %}{{ x }} {{ y }} {{ z is same as(null) ? 'null' : z }}
--DATA--
return []
--EXPECT--
@@ -18,3 +24,9 @@ return []
4
aa
94
# Array destructuring
Fabien Potencier
49
second
one two null