mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-05 15:07:11 +00:00
Add loop.changed, loop.previous, loop.next, and loop.cycle variables
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# 4.0.0 (2024-XX-XX)
|
||||
|
||||
* 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
|
||||
* Make `Environment::getGlobals()` private
|
||||
|
||||
+41
-3
@@ -55,9 +55,9 @@ The ``loop`` variable
|
||||
|
||||
Inside of a ``for`` loop block you can access some special variables:
|
||||
|
||||
===================== =============================================================
|
||||
===================== ========================================================================
|
||||
Variable Description
|
||||
===================== =============================================================
|
||||
===================== ========================================================================
|
||||
``loop.index`` The current iteration of the loop. (1 indexed)
|
||||
``loop.index0`` The current iteration of the loop. (0 indexed)
|
||||
``loop.revindex`` The number of iterations from the end of the loop (1 indexed)
|
||||
@@ -66,7 +66,11 @@ Variable Description
|
||||
``loop.last`` True if last iteration
|
||||
``loop.length`` The number of items in the sequence
|
||||
``loop.parent`` The parent context
|
||||
===================== =============================================================
|
||||
``loop.cycle`` Cycle over a sequence of values
|
||||
``loop.changed`` True if previously called with a different value or if not called yet
|
||||
``loop.previous`` The value from the previous iteration (``null`` for the first iteration)
|
||||
``loop.next`` The value from the next iteration (``null`` for the first iteration)
|
||||
===================== ========================================================================
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
@@ -74,6 +78,40 @@ Variable Description
|
||||
{{ loop.index }} - {{ user.username }}
|
||||
{% endfor %}
|
||||
|
||||
Use ``loop.cycle`` to cycle among a list of values:
|
||||
|
||||
.. code-block:: html+twig
|
||||
|
||||
{% for row in rows %}
|
||||
<li class="{{ loop.cycle('odd', 'even') }}">{{ row }}</li>
|
||||
{% endfor %}
|
||||
|
||||
Use ``loop.previous`` and ``loop.next`` to compare the current value with the
|
||||
previous or next values:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{% for value in values %}
|
||||
{% if not loop.first and value > loop.previous %}
|
||||
The value just increased!
|
||||
{% endif %}
|
||||
{{ value }}
|
||||
{% if not loop.last and loop.next > value %}
|
||||
The value will increase even more!
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
Use ``loop.changed`` to check if the value has changed since the last iteration:
|
||||
|
||||
.. code-block:: html+twig
|
||||
|
||||
{% for entry in entries %}
|
||||
{% if loop.changed(entry.category) %}
|
||||
<h2>{{ entry.category }}</h2>
|
||||
{% endif %}
|
||||
<p>{{ entry.message }}</p>
|
||||
{% endfor %}
|
||||
|
||||
.. note::
|
||||
|
||||
When the underlying PHP iterator is not countable, the ``loop.length``,
|
||||
|
||||
@@ -42,7 +42,7 @@ class ForNode extends Node
|
||||
$compiler
|
||||
->addDebugInfo($this)
|
||||
->write("\$context['_parent'] = \$context;\n")
|
||||
->write("\$$loopName = new \Twig\Runtime\Loop(")
|
||||
->write("\$$loopName = new \Twig\Runtime\LoopIterator(")
|
||||
->subcompile($this->getNode('seq'))
|
||||
->raw(");\n")
|
||||
;
|
||||
|
||||
@@ -20,7 +20,9 @@ namespace Twig\Runtime;
|
||||
*/
|
||||
final class LoopContext
|
||||
{
|
||||
public function __construct(private Loop $loop, private $parent)
|
||||
private mixed $lastChanged;
|
||||
|
||||
public function __construct(private LoopIterator $loop, private $parent)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -63,4 +65,36 @@ final class LoopContext
|
||||
{
|
||||
return $this->loop->isLast();
|
||||
}
|
||||
|
||||
public function hasChanged(mixed $value): bool
|
||||
{
|
||||
if (!isset($this->lastChanged) || $value !== $this->lastChanged) {
|
||||
$this->lastChanged = $value;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getPrevious(): mixed
|
||||
{
|
||||
$previous = $this->loop->getPrevious();
|
||||
|
||||
return $previous['valid'] ? $previous['value'] : null;
|
||||
}
|
||||
|
||||
public function getNext(): mixed
|
||||
{
|
||||
$next = $this->loop->getNext();
|
||||
|
||||
return $next['valid'] ? $next['value'] : null;
|
||||
}
|
||||
|
||||
public function cycle($value, ...$values): mixed
|
||||
{
|
||||
array_unshift($values, $value);
|
||||
|
||||
return $values[$this->getIndex0() % count($values)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@ use Twig\Error\RuntimeError;
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Loop implements \Iterator
|
||||
final class LoopIterator implements \Iterator
|
||||
{
|
||||
private \Iterator $seq;
|
||||
private int $index0;
|
||||
private int $length;
|
||||
private bool $peek = false;
|
||||
private array $previous = [];
|
||||
private array $current = [];
|
||||
private array $next = [];
|
||||
|
||||
public function __construct($seq)
|
||||
{
|
||||
@@ -35,33 +37,38 @@ final class Loop implements \Iterator
|
||||
|
||||
public function current(): mixed
|
||||
{
|
||||
return $this->seq->current();
|
||||
return $this->current['value'];
|
||||
}
|
||||
|
||||
public function key(): mixed
|
||||
{
|
||||
return $this->seq->key();
|
||||
return $this->current['key'];
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
if ($this->peek) {
|
||||
$this->peek = false;
|
||||
$this->previous = $this->current;
|
||||
if ($this->next) {
|
||||
$this->next = [];
|
||||
} else {
|
||||
$this->seq->next();
|
||||
}
|
||||
$this->current = ['valid' => $this->seq->valid(), 'key' => $this->seq->key(), 'value' => $this->seq->current()];
|
||||
++$this->index0;
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->seq->rewind();
|
||||
$this->previous = ['valid' => false, 'key' => null, 'value' => null];
|
||||
$this->current = ['valid' => $this->seq->valid(), 'key' => $this->seq->key(), 'value' => $this->seq->current()];
|
||||
$this->next = [];
|
||||
$this->index0 = 0;
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->seq->valid();
|
||||
return $this->current['valid'];
|
||||
}
|
||||
|
||||
public function iterated(): bool
|
||||
@@ -89,11 +96,26 @@ final class Loop implements \Iterator
|
||||
|
||||
public function isLast(): bool
|
||||
{
|
||||
if (!$this->peek) {
|
||||
return !$this->peek()['valid'];
|
||||
}
|
||||
|
||||
public function getPrevious(): array
|
||||
{
|
||||
return $this->previous;
|
||||
}
|
||||
|
||||
public function getNext(): array
|
||||
{
|
||||
return $this->peek();
|
||||
}
|
||||
|
||||
public function peek(): array
|
||||
{
|
||||
if (!$this->next) {
|
||||
$this->seq->next();
|
||||
$this->peek = true;
|
||||
$this->next = ['valid' => $this->seq->valid(), 'key' => $this->seq->key(), 'value' => $this->seq->current()];
|
||||
}
|
||||
|
||||
return !$this->seq->valid();
|
||||
return $this->next;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
--TEST--
|
||||
"for" tag exposes a loop.changed function
|
||||
--TEMPLATE--
|
||||
{% for entry in entries %}
|
||||
{%- if loop.changed(entry.category) -%}
|
||||
<h2>{{ entry.category }}</h2>
|
||||
{%~ endif %}
|
||||
<p>{{ entry.message }}</p>
|
||||
{% endfor %}
|
||||
--DATA--
|
||||
return ['entries' => [
|
||||
[ 'category' => 'cat1', 'message' => 'Cat1 message' ],
|
||||
[ 'category' => 'cat1', 'message' => 'Another cat1 message' ],
|
||||
[ 'category' => 'cat2', 'message' => 'Cat2 message' ],
|
||||
[ 'category' => 'cat3', 'message' => 'Yet another category of messages' ],
|
||||
]]
|
||||
--EXPECT--
|
||||
<h2>cat1</h2>
|
||||
<p>Cat1 message</p>
|
||||
<p>Another cat1 message</p>
|
||||
<h2>cat2</h2>
|
||||
<p>Cat2 message</p>
|
||||
<h2>cat3</h2>
|
||||
<p>Yet another category of messages</p>
|
||||
@@ -0,0 +1,14 @@
|
||||
--TEST--
|
||||
"for" tag exposes a loop.cycle function
|
||||
--TEMPLATE--
|
||||
{% for row in [1, 2, 3, 4, 5] %}
|
||||
<li class="{{ loop.cycle('odd', 'even') }}">{{ row }}</li>
|
||||
{% endfor %}
|
||||
--DATA--
|
||||
return []
|
||||
--EXPECT--
|
||||
<li class="odd">1</li>
|
||||
<li class="even">2</li>
|
||||
<li class="odd">3</li>
|
||||
<li class="even">4</li>
|
||||
<li class="odd">5</li>
|
||||
@@ -0,0 +1,27 @@
|
||||
--TEST--
|
||||
"for" tag exposes loop.previous and loop.next functions
|
||||
--TEMPLATE--
|
||||
{% for value in [1, 3, 1, 5, 3] %}
|
||||
|
||||
{{~ loop.previous ?? ' ' -}}
|
||||
{% if not loop.first and value > loop.previous -%}
|
||||
<
|
||||
{%- else -%}
|
||||
{{ loop.first ? ' ' : '>' -}}
|
||||
{%- endif -%}
|
||||
[{{~ value }}]
|
||||
{%- if not loop.last and loop.next > value -%}
|
||||
<
|
||||
{%- else -%}
|
||||
{{ loop.last ? ' ' : '>' -}}
|
||||
{%- endif -%}
|
||||
{{ loop.next ?? ' ' -}}
|
||||
{% endfor %}
|
||||
--DATA--
|
||||
return []
|
||||
--EXPECT--
|
||||
[1]<3
|
||||
1<[3]>1
|
||||
3>[1]<5
|
||||
1<[5]>3
|
||||
5>[3]
|
||||
@@ -57,7 +57,7 @@ class ForTest extends NodeTestCase
|
||||
$tests[] = [$node, <<<EOF
|
||||
// line 1
|
||||
\$context['_parent'] = \$context;
|
||||
\$__internal_compile_0 = new \Twig\Runtime\Loop({$this->getVariableGetter('items')});
|
||||
\$__internal_compile_0 = new \Twig\Runtime\LoopIterator({$this->getVariableGetter('items')});
|
||||
foreach (\$__internal_compile_0 as \$context["key"] => \$context["item"]) {
|
||||
yield {$this->getVariableGetter('foo')};
|
||||
}
|
||||
@@ -78,7 +78,7 @@ EOF
|
||||
$tests[] = [$node, <<<EOF
|
||||
// line 1
|
||||
\$context['_parent'] = \$context;
|
||||
\$__internal_compile_0 = new \Twig\Runtime\Loop({$this->getVariableGetter('values')});
|
||||
\$__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')};
|
||||
@@ -100,7 +100,7 @@ EOF
|
||||
$tests[] = [$node, <<<EOF
|
||||
// line 1
|
||||
\$context['_parent'] = \$context;
|
||||
\$__internal_compile_0 = new \Twig\Runtime\Loop({$this->getVariableGetter('values')});
|
||||
\$__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')};
|
||||
@@ -122,7 +122,7 @@ EOF
|
||||
$tests[] = [$node, <<<EOF
|
||||
// line 1
|
||||
\$context['_parent'] = \$context;
|
||||
\$__internal_compile_0 = new \Twig\Runtime\Loop({$this->getVariableGetter('values')});
|
||||
\$__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')};
|
||||
|
||||
Reference in New Issue
Block a user