mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-16 12:26:30 +00:00
Add support for recursive loops
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# 4.0.0 (2024-XX-XX)
|
# 4.0.0 (2024-XX-XX)
|
||||||
|
|
||||||
|
* Add support for recursive loops (via `loop.recurse()`)
|
||||||
* Add `loop.changed`, `loop.previous`, `loop.next`, and `loop.cycle` variables
|
* Add `loop.changed`, `loop.previous`, `loop.next`, and `loop.cycle` variables
|
||||||
* Make `loop.last` always available (even for non-countable iterators)
|
* Make `loop.last` always available (even for non-countable iterators)
|
||||||
* Change the compilation of `for` loops to throw an exception when a `loop.*` variable is not defined
|
* Change the compilation of `for` loops to throw an exception when a `loop.*` variable is not defined
|
||||||
|
|||||||
@@ -110,6 +110,24 @@ replacement block by using ``else``:
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
Recursive Loops
|
||||||
|
---------------
|
||||||
|
|
||||||
|
To use loops recursively, pass the iterable you want to recurse to the
|
||||||
|
``loop()`` function; the following example shows how to use it for a recursive
|
||||||
|
sitemap:
|
||||||
|
|
||||||
|
.. code-block:: html+twig
|
||||||
|
|
||||||
|
<ul class="sitemap">
|
||||||
|
{%- for item in sitemap %}
|
||||||
|
<li>{{ item.title }}
|
||||||
|
{%- if item.children -%}
|
||||||
|
<ul class="submenu">{{ loop(item.children) }}</ul>
|
||||||
|
{%- endif %}</li>
|
||||||
|
{%- endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
The ``loop`` Object
|
The ``loop`` Object
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
@@ -132,6 +150,8 @@ Variable Description
|
|||||||
``loop.parent`` The parent context
|
``loop.parent`` The parent context
|
||||||
``loop.previous`` The value from the previous iteration (``null`` for the first iteration)
|
``loop.previous`` The value from the previous iteration (``null`` for the first iteration)
|
||||||
``loop.next`` The value from the next iteration (``null`` for the last iteration)
|
``loop.next`` The value from the next iteration (``null`` for the last iteration)
|
||||||
|
``loop.depth`` Deep level of a recursive loop (1 indexed)
|
||||||
|
``loop.depth0`` Deep level of a recursive loop (0 indexed)
|
||||||
===================== ========================================================================
|
===================== ========================================================================
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
@@ -175,6 +195,7 @@ Function Description
|
|||||||
===================== ========================================================================
|
===================== ========================================================================
|
||||||
``loop.cycle()`` Cycle over a sequence of values
|
``loop.cycle()`` Cycle over a sequence of values
|
||||||
``loop.changed()`` True if previously called with a different value or if not called yet
|
``loop.changed()`` True if previously called with a different value or if not called yet
|
||||||
|
``loop()`` Allows to iterate over a nested sequence/mapping
|
||||||
===================== ========================================================================
|
===================== ========================================================================
|
||||||
|
|
||||||
Use ``loop.cycle()`` to cycle among a list of values:
|
Use ``loop.cycle()`` to cycle among a list of values:
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ use Twig\Node\Expression\Test\SameasTest;
|
|||||||
use Twig\Node\Expression\Unary\NegUnary;
|
use Twig\Node\Expression\Unary\NegUnary;
|
||||||
use Twig\Node\Expression\Unary\NotUnary;
|
use Twig\Node\Expression\Unary\NotUnary;
|
||||||
use Twig\Node\Expression\Unary\PosUnary;
|
use Twig\Node\Expression\Unary\PosUnary;
|
||||||
|
use Twig\NodeVisitor\ForNodeVisitor;
|
||||||
use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
|
use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
|
||||||
use Twig\Source;
|
use Twig\Source;
|
||||||
use Twig\Template;
|
use Twig\Template;
|
||||||
@@ -268,7 +269,10 @@ final class CoreExtension extends AbstractExtension
|
|||||||
|
|
||||||
public function getNodeVisitors(): array
|
public function getNodeVisitors(): array
|
||||||
{
|
{
|
||||||
return [new MacroAutoImportNodeVisitor()];
|
return [
|
||||||
|
new MacroAutoImportNodeVisitor(),
|
||||||
|
new ForNodeVisitor(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOperators(): array
|
public function getOperators(): array
|
||||||
|
|||||||
+14
-2
@@ -38,17 +38,21 @@ class ForNode extends Node
|
|||||||
public function compile(Compiler $compiler): void
|
public function compile(Compiler $compiler): void
|
||||||
{
|
{
|
||||||
$iteratorVar = $compiler->getVarName();
|
$iteratorVar = $compiler->getVarName();
|
||||||
|
$functionVar = $compiler->getVarName();
|
||||||
|
|
||||||
$compiler
|
$compiler
|
||||||
->addDebugInfo($this)
|
->addDebugInfo($this)
|
||||||
->write("\$context['_parent'] = \$context;\n")
|
|
||||||
->write("\$$iteratorVar = new \Twig\Runtime\LoopIterator(")
|
->write("\$$iteratorVar = new \Twig\Runtime\LoopIterator(")
|
||||||
->subcompile($this->getNode('seq'))
|
->subcompile($this->getNode('seq'))
|
||||||
->raw(");\n")
|
->raw(");\n")
|
||||||
|
->write("\$$functionVar = function (\$$iteratorVar, &\$context, \$blocks, &\$$functionVar, \$depth) {\n")
|
||||||
|
->indent()
|
||||||
|
->write("\$macros = \$this->macros;\n")
|
||||||
|
->write("\$context['_parent'] = \$context;\n")
|
||||||
;
|
;
|
||||||
|
|
||||||
if ($this->getAttribute('with_loop')) {
|
if ($this->getAttribute('with_loop')) {
|
||||||
$compiler->write("\$context['loop'] = new \Twig\Runtime\LoopContext(\$$iteratorVar, \$context['_parent']);\n");
|
$compiler->write("\$context['loop'] = new \Twig\Runtime\LoopContext(\$$iteratorVar, \$context['_parent'], \$blocks, \$$functionVar, \$depth);\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
$compiler
|
$compiler
|
||||||
@@ -80,5 +84,13 @@ class ForNode extends Node
|
|||||||
|
|
||||||
// keep the values set in the inner context for variables defined in the outer context
|
// keep the values set in the inner context for variables defined in the outer context
|
||||||
$compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n");
|
$compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n");
|
||||||
|
|
||||||
|
$compiler
|
||||||
|
->write("return; yield;\n")
|
||||||
|
->outdent()
|
||||||
|
->write("};\n")
|
||||||
|
->write("\Closure::bind(\$$functionVar, \$this, self::class);\n")
|
||||||
|
->write("yield from \$$functionVar(\$$iteratorVar, \$context, \$blocks, \$$functionVar, 0);\n")
|
||||||
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?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\NodeVisitor;
|
||||||
|
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Node\Expression\ConstantExpression;
|
||||||
|
use Twig\Node\Expression\GetAttrExpression;
|
||||||
|
use Twig\Node\Expression\NameExpression;
|
||||||
|
use Twig\Node\ForNode;
|
||||||
|
use Twig\Node\Node;
|
||||||
|
use Twig\Node\PrintNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class ForNodeVisitor implements NodeVisitorInterface
|
||||||
|
{
|
||||||
|
private int $loops = 0;
|
||||||
|
|
||||||
|
public function enterNode(Node $node, Environment $env): Node
|
||||||
|
{
|
||||||
|
if ($node instanceof ForNode) {
|
||||||
|
++$this->loops;
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
} elseif (!$this->loops) {
|
||||||
|
// we are outside a loop
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$node instanceof PrintNode) {
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We look for exactly {{ loop.recurse(...) }}
|
||||||
|
$exprNode = $node->getNode('expr');
|
||||||
|
if (
|
||||||
|
$exprNode instanceof GetAttrExpression
|
||||||
|
&& $exprNode->getNode('node') instanceof NameExpression
|
||||||
|
&& 'loop' === $exprNode->getNode('node')->getAttribute('name')
|
||||||
|
&& $exprNode->getNode('attribute') instanceof ConstantExpression
|
||||||
|
&& 'recurse' === $exprNode->getNode('attribute')->getAttribute('value')
|
||||||
|
) {
|
||||||
|
$exprNode->setAttribute('is_generator', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function leaveNode(Node $node, Environment $env): ?Node
|
||||||
|
{
|
||||||
|
if ($node instanceof ForNode) {
|
||||||
|
--$this->loops;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPriority(): int
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,9 @@ final class LoopContext
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private LoopIterator $loop,
|
private LoopIterator $loop,
|
||||||
private $parent,
|
private $parent,
|
||||||
|
private $blocks,
|
||||||
|
private $recurseFunc,
|
||||||
|
private $depth,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,4 +102,19 @@ final class LoopContext
|
|||||||
|
|
||||||
return $values[$this->getIndex0() % count($values)];
|
return $values[$this->getIndex0() % count($values)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function recurse($iterator): \Generator
|
||||||
|
{
|
||||||
|
yield from ($this->recurseFunc)(new LoopIterator($iterator), $this->parent, $this->blocks, $this->recurseFunc, $this->depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDepth0(): int
|
||||||
|
{
|
||||||
|
return $this->depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDepth(): int
|
||||||
|
{
|
||||||
|
return $this->depth + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
--DATA--
|
--DATA--
|
||||||
return ['foo' => 'bar']
|
return ['foo' => 'bar']
|
||||||
--EXPECT--
|
--EXPECT--
|
||||||
foo,global,_parent,
|
foo,global,
|
||||||
global,_parent,
|
global,
|
||||||
foo,global,foo1,_parent,
|
foo,global,foo1,
|
||||||
foo1,global,_parent,
|
foo1,global,
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
--DATA--
|
--DATA--
|
||||||
return ['vars1' => ['foo1' => 'bar'], 'vars2' => new ArrayObject(['foo2' => 'bar'])]
|
return ['vars1' => ['foo1' => 'bar'], 'vars2' => new ArrayObject(['foo2' => 'bar'])]
|
||||||
--EXPECT--
|
--EXPECT--
|
||||||
vars1,vars2,global,_parent,
|
vars1,vars2,global,
|
||||||
global,_parent,
|
global,
|
||||||
vars1,vars2,global,foo1,_parent,
|
vars1,vars2,global,foo1,
|
||||||
foo1,global,_parent,
|
foo1,global,
|
||||||
vars1,vars2,global,foo2,_parent,
|
vars1,vars2,global,foo2,
|
||||||
foo2,global,_parent,
|
foo2,global,
|
||||||
|
|||||||
+58
-34
@@ -56,14 +56,20 @@ class ForTest extends NodeTestCase
|
|||||||
|
|
||||||
$tests[] = [$node, <<<EOF
|
$tests[] = [$node, <<<EOF
|
||||||
// line 1
|
// line 1
|
||||||
\$context['_parent'] = \$context;
|
|
||||||
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('items')});
|
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('items')});
|
||||||
foreach (\$__internal_compile_0 as \$context["key"] => \$context["item"]) {
|
\$__internal_compile_1 = function (\$__internal_compile_0, &\$context, \$blocks, &\$__internal_compile_1, \$depth) {
|
||||||
yield {$this->getVariableGetter('foo')};
|
\$macros = \$this->macros;
|
||||||
}
|
\$context['_parent'] = \$context;
|
||||||
\$_parent = \$context['_parent'];
|
foreach (\$__internal_compile_0 as \$context["key"] => \$context["item"]) {
|
||||||
unset(\$context['key'], \$context['item'], \$context['_parent'], \$context['loop']);
|
yield {$this->getVariableGetter('foo')};
|
||||||
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
}
|
||||||
|
\$_parent = \$context['_parent'];
|
||||||
|
unset(\$context['key'], \$context['item'], \$context['_parent'], \$context['loop']);
|
||||||
|
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
||||||
|
return; yield;
|
||||||
|
};
|
||||||
|
\Closure::bind(\$__internal_compile_1, \$this, self::class);
|
||||||
|
yield from \$__internal_compile_1(\$__internal_compile_0, \$context, \$blocks, \$__internal_compile_1, 0);
|
||||||
EOF
|
EOF
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -77,15 +83,21 @@ EOF
|
|||||||
|
|
||||||
$tests[] = [$node, <<<EOF
|
$tests[] = [$node, <<<EOF
|
||||||
// line 1
|
// line 1
|
||||||
\$context['_parent'] = \$context;
|
|
||||||
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('values')});
|
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('values')});
|
||||||
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent']);
|
\$__internal_compile_1 = function (\$__internal_compile_0, &\$context, \$blocks, &\$__internal_compile_1, \$depth) {
|
||||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
\$macros = \$this->macros;
|
||||||
yield {$this->getVariableGetter('foo')};
|
\$context['_parent'] = \$context;
|
||||||
}
|
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent'], \$blocks, \$__internal_compile_1, \$depth);
|
||||||
\$_parent = \$context['_parent'];
|
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||||
unset(\$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
|
yield {$this->getVariableGetter('foo')};
|
||||||
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
}
|
||||||
|
\$_parent = \$context['_parent'];
|
||||||
|
unset(\$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
|
||||||
|
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
||||||
|
return; yield;
|
||||||
|
};
|
||||||
|
\Closure::bind(\$__internal_compile_1, \$this, self::class);
|
||||||
|
yield from \$__internal_compile_1(\$__internal_compile_0, \$context, \$blocks, \$__internal_compile_1, 0);
|
||||||
EOF
|
EOF
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -99,15 +111,21 @@ EOF
|
|||||||
|
|
||||||
$tests[] = [$node, <<<EOF
|
$tests[] = [$node, <<<EOF
|
||||||
// line 1
|
// line 1
|
||||||
\$context['_parent'] = \$context;
|
|
||||||
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('values')});
|
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('values')});
|
||||||
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent']);
|
\$__internal_compile_1 = function (\$__internal_compile_0, &\$context, \$blocks, &\$__internal_compile_1, \$depth) {
|
||||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
\$macros = \$this->macros;
|
||||||
yield {$this->getVariableGetter('foo')};
|
\$context['_parent'] = \$context;
|
||||||
}
|
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent'], \$blocks, \$__internal_compile_1, \$depth);
|
||||||
\$_parent = \$context['_parent'];
|
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||||
unset(\$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
|
yield {$this->getVariableGetter('foo')};
|
||||||
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
}
|
||||||
|
\$_parent = \$context['_parent'];
|
||||||
|
unset(\$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
|
||||||
|
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
||||||
|
return; yield;
|
||||||
|
};
|
||||||
|
\Closure::bind(\$__internal_compile_1, \$this, self::class);
|
||||||
|
yield from \$__internal_compile_1(\$__internal_compile_0, \$context, \$blocks, \$__internal_compile_1, 0);
|
||||||
EOF
|
EOF
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -121,18 +139,24 @@ EOF
|
|||||||
|
|
||||||
$tests[] = [$node, <<<EOF
|
$tests[] = [$node, <<<EOF
|
||||||
// line 1
|
// line 1
|
||||||
\$context['_parent'] = \$context;
|
|
||||||
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('values')});
|
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('values')});
|
||||||
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent']);
|
\$__internal_compile_1 = function (\$__internal_compile_0, &\$context, \$blocks, &\$__internal_compile_1, \$depth) {
|
||||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
\$macros = \$this->macros;
|
||||||
yield {$this->getVariableGetter('foo')};
|
\$context['_parent'] = \$context;
|
||||||
}
|
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent'], \$blocks, \$__internal_compile_1, \$depth);
|
||||||
if (0 === \$__internal_compile_0->getIndex0()) {
|
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||||
yield {$this->getVariableGetter('foo')};
|
yield {$this->getVariableGetter('foo')};
|
||||||
}
|
}
|
||||||
\$_parent = \$context['_parent'];
|
if (0 === \$__internal_compile_0->getIndex0()) {
|
||||||
unset(\$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
|
yield {$this->getVariableGetter('foo')};
|
||||||
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
}
|
||||||
|
\$_parent = \$context['_parent'];
|
||||||
|
unset(\$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
|
||||||
|
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
||||||
|
return; yield;
|
||||||
|
};
|
||||||
|
\Closure::bind(\$__internal_compile_1, \$this, self::class);
|
||||||
|
yield from \$__internal_compile_1(\$__internal_compile_0, \$context, \$blocks, \$__internal_compile_1, 0);
|
||||||
EOF
|
EOF
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user