Fix Traversable sequence destructuring semantics

This commit is contained in:
Fabien Potencier
2026-08-23 09:29:55 +02:00
parent 9de1b3db98
commit e2014eb92a
5 changed files with 70 additions and 11 deletions
+1
View File
@@ -1,6 +1,7 @@
# 3.29.0 (2026-XX-XX)
* Add documentation comments to attach metadata to nodes (experimental)
* Fix sequence destructuring of iterators throwing a `TypeError`
* Add `TempestMarkdown` to use `tempest/markdown` as the `markdown_to_html` converter
* Fix imported macros not resolving their own template-level macro imports
* Add the `include_only` function to render a template without giving it access to the current context
+9
View File
@@ -1210,6 +1210,15 @@ You can skip values by leaving a slot empty:
{# only assign the second value #}
{% do [, last] = ['Fabien', 'Potencier'] %}
.. versionadded:: 3.29
Support for destructuring iterators was introduced in Twig 3.29.
Sequence destructuring also works with iterators (any ``Traversable``
value). Values are extracted in iteration order and keys are ignored. The
iterator is consumed lazily: only as many values as there are variables are
fetched, and the expression returns the iterator itself.
Object Destructuring
~~~~~~~~~~~~~~~~~~~~
+33
View File
@@ -1401,6 +1401,39 @@ final class CoreExtension extends AbstractExtension
return $preserveKeys ? $seq : array_values($seq);
}
/**
* @param list<string|null> $names
*
* @internal
*/
public static function destructureSequence(array &$context, array $names, \Traversable $sequence): \Traversable
{
$count = \count($names);
if (0 === $count) {
return $sequence;
}
$i = 0;
foreach ($sequence as $value) {
$name = $names[$i];
if (null !== $name) {
$context[$name] = $value;
}
if (++$i === $count) {
return $sequence;
}
}
for (; $i < $count; ++$i) {
$name = $names[$i];
if (null !== $name) {
$context[$name] = null;
}
}
return $sequence;
}
/**
* Checks if a variable is empty.
*
@@ -48,7 +48,14 @@ class SequenceDestructuringSetBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);
$compiler->raw('[');
$var = '$'.$compiler->getVarName();
$compiler
->raw('(('.$var.' = ')
->subcompile($this->getNode('right'))
->raw(') instanceof \Traversable ? CoreExtension::destructureSequence($context, ')
->repr($this->variables)
->raw(', '.$var.') : ([')
;
foreach ($this->variables as $i => $name) {
if ($i) {
$compiler->raw(', ');
@@ -57,15 +64,11 @@ class SequenceDestructuringSetBinary extends AbstractBinary
$compiler->raw('$context[')->repr($name)->raw(']');
}
}
$compiler->raw('] = array_pad((');
$var = '$'.$compiler->getVarName();
$compiler
->raw($var.' = ')
->subcompile($this->getNode('right'))
->raw(') instanceof \Traversable ? iterator_to_array('.$var.') : '.$var)
->raw(', ')
->raw('] = array_pad('.$var.', ')
->repr(\count($this->variables))
->raw(', null)');
->raw(', null)))')
;
}
public function operator(Compiler $compiler): Compiler
+16 -3
View File
@@ -23,7 +23,8 @@ Twig supports the "=" operator (assignment)
{% do {name} = user_obj %}{{ name }}
# Array destructuring from a Traversable
{% do [p, q] = pair_traversable %}{{ p }} {{ q }}
{% do destructured = ([p, q] = pair_traversable) %}{{ p }} {{ q }} {{ pair_traversable.yielded }} {{ destructured is same as(pair_traversable) ? 'same' : 'different' }}
{% do [r, , t] = short_traversable %}{{ r }} {{ t is same as(null) ? 'null' : t }}
# Object destructuring with renaming
{% do {name: userName, email: userEmail} = user %}{{ userName }} {{ userEmail }}
@@ -38,7 +39,18 @@ return [
public function getName() { return 'Fabien'; }
},
'null_obj' => null,
'pair_traversable' => new ArrayIterator(['Fabien', 'Potencier']),
'pair_traversable' => new class implements IteratorAggregate {
public int $yielded = 0;
public function getIterator(): Traversable
{
foreach ([5 => 'Fabien', 8 => 'Potencier', 13 => 'Ignored'] as $key => $value) {
++$this->yielded;
yield $key => $value;
}
}
},
'short_traversable' => new ArrayIterator(['one']),
]
--EXPECT--
4
@@ -59,7 +71,8 @@ Fabien Potencier
Fabien
# Array destructuring from a Traversable
Fabien Potencier
Fabien Potencier 2 same
one null
# Object destructuring with renaming
Fabien fabien@example.com