mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-15 03:46:39 +00:00
feature #4893 Fix array destructuring from a Traversable (iliaal, fabpot)
This PR was merged into the 3.x branch.
Discussion
----------
Fix array destructuring from a Traversable
Sequence destructuring compiled its right-hand side straight into `array_pad()`, which only accepts arrays. Destructuring from any `Traversable` crashed with an uncaught `TypeError` at render time:
```twig
{% do [a, b] = items %}
```
with `items` being e.g. an `ArrayIterator` or a generator:
```
array_pad(): Argument #1 ($array) must be of type array, ArrayIterator given
```
This change compiles the right-hand side through an `iterator_to_array()` coercion when it is a `Traversable`, matching how spread (`[...traversable]`), `merge`, and `slice` already accept Traversables:
```php
[$context["a"], $context["b"]] = array_pad(($_v0 = ($context["items"] ?? null)) instanceof \Traversable ? iterator_to_array($_v0) : $_v0, 2, null);
```
Behavior for arrays and other types is unchanged; scalars still fail as before (now via the same `array_pad` TypeError path).
Commits
-------
e2014eb92a Fix Traversable sequence destructuring semantics
9de1b3db98 Fix array destructuring from a Traversable
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -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,7 +64,11 @@ class SequenceDestructuringSetBinary extends AbstractBinary
|
||||
$compiler->raw('$context[')->repr($name)->raw(']');
|
||||
}
|
||||
}
|
||||
$compiler->raw('] = array_pad(')->subcompile($this->getNode('right'))->raw(', ')->repr(\count($this->variables))->raw(', null)');
|
||||
$compiler
|
||||
->raw('] = array_pad('.$var.', ')
|
||||
->repr(\count($this->variables))
|
||||
->raw(', null)))')
|
||||
;
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
|
||||
@@ -22,6 +22,10 @@ Twig supports the "=" operator (assignment)
|
||||
{% do {first_name, last_name} = user_map %}{{ first_name }} {{ last_name }}
|
||||
{% do {name} = user_obj %}{{ name }}
|
||||
|
||||
# Array destructuring from a Traversable
|
||||
{% 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 }}
|
||||
{% do {first_name: first, last_name: last} = user_map %}{{ first }} {{ last }}
|
||||
@@ -34,7 +38,19 @@ return [
|
||||
'user_obj' => new class {
|
||||
public function getName() { return 'Fabien'; }
|
||||
},
|
||||
'null_obj' => null
|
||||
'null_obj' => null,
|
||||
'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
|
||||
@@ -54,6 +70,10 @@ Fabien fabien@example.com
|
||||
Fabien Potencier
|
||||
Fabien
|
||||
|
||||
# Array destructuring from a Traversable
|
||||
Fabien Potencier 2 same
|
||||
one null
|
||||
|
||||
# Object destructuring with renaming
|
||||
Fabien fabien@example.com
|
||||
Fabien Potencier
|
||||
|
||||
Reference in New Issue
Block a user