mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-14 19:36:43 +00:00
Add support for recursive loops
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# 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
|
||||
* 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
|
||||
|
||||
@@ -110,6 +110,24 @@ replacement block by using ``else``:
|
||||
{% endfor %}
|
||||
</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
|
||||
-------------------
|
||||
|
||||
@@ -132,6 +150,8 @@ Variable Description
|
||||
``loop.parent`` The parent context
|
||||
``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.depth`` Deep level of a recursive loop (1 indexed)
|
||||
``loop.depth0`` Deep level of a recursive loop (0 indexed)
|
||||
===================== ========================================================================
|
||||
|
||||
.. note::
|
||||
@@ -175,6 +195,7 @@ Function Description
|
||||
===================== ========================================================================
|
||||
``loop.cycle()`` Cycle over a sequence of values
|
||||
``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:
|
||||
|
||||
@@ -56,6 +56,7 @@ use Twig\Node\Expression\Test\SameasTest;
|
||||
use Twig\Node\Expression\Unary\NegUnary;
|
||||
use Twig\Node\Expression\Unary\NotUnary;
|
||||
use Twig\Node\Expression\Unary\PosUnary;
|
||||
use Twig\NodeVisitor\ForNodeVisitor;
|
||||
use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
|
||||
use Twig\Source;
|
||||
use Twig\Template;
|
||||
@@ -268,7 +269,10 @@ final class CoreExtension extends AbstractExtension
|
||||
|
||||
public function getNodeVisitors(): array
|
||||
{
|
||||
return [new MacroAutoImportNodeVisitor()];
|
||||
return [
|
||||
new MacroAutoImportNodeVisitor(),
|
||||
new ForNodeVisitor(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getOperators(): array
|
||||
|
||||
+14
-2
@@ -38,17 +38,21 @@ class ForNode extends Node
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$iteratorVar = $compiler->getVarName();
|
||||
$functionVar = $compiler->getVarName();
|
||||
|
||||
$compiler
|
||||
->addDebugInfo($this)
|
||||
->write("\$context['_parent'] = \$context;\n")
|
||||
->write("\$$iteratorVar = new \Twig\Runtime\LoopIterator(")
|
||||
->subcompile($this->getNode('seq'))
|
||||
->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')) {
|
||||
$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
|
||||
@@ -80,5 +84,13 @@ class ForNode extends Node
|
||||
|
||||
// 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("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(
|
||||
private LoopIterator $loop,
|
||||
private $parent,
|
||||
private $blocks,
|
||||
private $recurseFunc,
|
||||
private $depth,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -99,4 +102,19 @@ final class LoopContext
|
||||
|
||||
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--
|
||||
return ['foo' => 'bar']
|
||||
--EXPECT--
|
||||
foo,global,_parent,
|
||||
global,_parent,
|
||||
foo,global,foo1,_parent,
|
||||
foo1,global,_parent,
|
||||
foo,global,
|
||||
global,
|
||||
foo,global,foo1,
|
||||
foo1,global,
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
--DATA--
|
||||
return ['vars1' => ['foo1' => 'bar'], 'vars2' => new ArrayObject(['foo2' => 'bar'])]
|
||||
--EXPECT--
|
||||
vars1,vars2,global,_parent,
|
||||
global,_parent,
|
||||
vars1,vars2,global,foo1,_parent,
|
||||
foo1,global,_parent,
|
||||
vars1,vars2,global,foo2,_parent,
|
||||
foo2,global,_parent,
|
||||
vars1,vars2,global,
|
||||
global,
|
||||
vars1,vars2,global,foo1,
|
||||
foo1,global,
|
||||
vars1,vars2,global,foo2,
|
||||
foo2,global,
|
||||
|
||||
+58
-34
@@ -56,14 +56,20 @@ class ForTest extends NodeTestCase
|
||||
|
||||
$tests[] = [$node, <<<EOF
|
||||
// line 1
|
||||
\$context['_parent'] = \$context;
|
||||
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('items')});
|
||||
foreach (\$__internal_compile_0 as \$context["key"] => \$context["item"]) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
\$_parent = \$context['_parent'];
|
||||
unset(\$context['key'], \$context['item'], \$context['_parent'], \$context['loop']);
|
||||
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
||||
\$__internal_compile_1 = function (\$__internal_compile_0, &\$context, \$blocks, &\$__internal_compile_1, \$depth) {
|
||||
\$macros = \$this->macros;
|
||||
\$context['_parent'] = \$context;
|
||||
foreach (\$__internal_compile_0 as \$context["key"] => \$context["item"]) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
\$_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
|
||||
];
|
||||
|
||||
@@ -77,15 +83,21 @@ EOF
|
||||
|
||||
$tests[] = [$node, <<<EOF
|
||||
// line 1
|
||||
\$context['_parent'] = \$context;
|
||||
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('values')});
|
||||
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent']);
|
||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
\$_parent = \$context['_parent'];
|
||||
unset(\$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
|
||||
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
||||
\$__internal_compile_1 = function (\$__internal_compile_0, &\$context, \$blocks, &\$__internal_compile_1, \$depth) {
|
||||
\$macros = \$this->macros;
|
||||
\$context['_parent'] = \$context;
|
||||
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent'], \$blocks, \$__internal_compile_1, \$depth);
|
||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
\$_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
|
||||
];
|
||||
|
||||
@@ -99,15 +111,21 @@ EOF
|
||||
|
||||
$tests[] = [$node, <<<EOF
|
||||
// line 1
|
||||
\$context['_parent'] = \$context;
|
||||
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('values')});
|
||||
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent']);
|
||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
\$_parent = \$context['_parent'];
|
||||
unset(\$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
|
||||
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
||||
\$__internal_compile_1 = function (\$__internal_compile_0, &\$context, \$blocks, &\$__internal_compile_1, \$depth) {
|
||||
\$macros = \$this->macros;
|
||||
\$context['_parent'] = \$context;
|
||||
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent'], \$blocks, \$__internal_compile_1, \$depth);
|
||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
\$_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
|
||||
];
|
||||
|
||||
@@ -121,18 +139,24 @@ EOF
|
||||
|
||||
$tests[] = [$node, <<<EOF
|
||||
// line 1
|
||||
\$context['_parent'] = \$context;
|
||||
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('values')});
|
||||
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent']);
|
||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
if (0 === \$__internal_compile_0->getIndex0()) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
\$_parent = \$context['_parent'];
|
||||
unset(\$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
|
||||
\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
|
||||
\$__internal_compile_1 = function (\$__internal_compile_0, &\$context, \$blocks, &\$__internal_compile_1, \$depth) {
|
||||
\$macros = \$this->macros;
|
||||
\$context['_parent'] = \$context;
|
||||
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent'], \$blocks, \$__internal_compile_1, \$depth);
|
||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
if (0 === \$__internal_compile_0->getIndex0()) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
\$_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
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user