mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-18 13:26:37 +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)
|
# 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)
|
* 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
|
||||||
* Make `Environment::getGlobals()` private
|
* 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:
|
Inside of a ``for`` loop block you can access some special variables:
|
||||||
|
|
||||||
===================== =============================================================
|
===================== ========================================================================
|
||||||
Variable Description
|
Variable Description
|
||||||
===================== =============================================================
|
===================== ========================================================================
|
||||||
``loop.index`` The current iteration of the loop. (1 indexed)
|
``loop.index`` The current iteration of the loop. (1 indexed)
|
||||||
``loop.index0`` The current iteration of the loop. (0 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)
|
``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.last`` True if last iteration
|
||||||
``loop.length`` The number of items in the sequence
|
``loop.length`` The number of items in the sequence
|
||||||
``loop.parent`` The parent context
|
``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
|
.. code-block:: twig
|
||||||
|
|
||||||
@@ -74,6 +78,40 @@ Variable Description
|
|||||||
{{ loop.index }} - {{ user.username }}
|
{{ loop.index }} - {{ user.username }}
|
||||||
{% endfor %}
|
{% 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::
|
.. note::
|
||||||
|
|
||||||
When the underlying PHP iterator is not countable, the ``loop.length``,
|
When the underlying PHP iterator is not countable, the ``loop.length``,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class ForNode extends Node
|
|||||||
$compiler
|
$compiler
|
||||||
->addDebugInfo($this)
|
->addDebugInfo($this)
|
||||||
->write("\$context['_parent'] = \$context;\n")
|
->write("\$context['_parent'] = \$context;\n")
|
||||||
->write("\$$loopName = new \Twig\Runtime\Loop(")
|
->write("\$$loopName = new \Twig\Runtime\LoopIterator(")
|
||||||
->subcompile($this->getNode('seq'))
|
->subcompile($this->getNode('seq'))
|
||||||
->raw(");\n")
|
->raw(");\n")
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ namespace Twig\Runtime;
|
|||||||
*/
|
*/
|
||||||
final class LoopContext
|
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();
|
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
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Loop implements \Iterator
|
final class LoopIterator implements \Iterator
|
||||||
{
|
{
|
||||||
private \Iterator $seq;
|
private \Iterator $seq;
|
||||||
private int $index0;
|
private int $index0;
|
||||||
private int $length;
|
private int $length;
|
||||||
private bool $peek = false;
|
private array $previous = [];
|
||||||
|
private array $current = [];
|
||||||
|
private array $next = [];
|
||||||
|
|
||||||
public function __construct($seq)
|
public function __construct($seq)
|
||||||
{
|
{
|
||||||
@@ -35,33 +37,38 @@ final class Loop implements \Iterator
|
|||||||
|
|
||||||
public function current(): mixed
|
public function current(): mixed
|
||||||
{
|
{
|
||||||
return $this->seq->current();
|
return $this->current['value'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function key(): mixed
|
public function key(): mixed
|
||||||
{
|
{
|
||||||
return $this->seq->key();
|
return $this->current['key'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function next(): void
|
public function next(): void
|
||||||
{
|
{
|
||||||
if ($this->peek) {
|
$this->previous = $this->current;
|
||||||
$this->peek = false;
|
if ($this->next) {
|
||||||
|
$this->next = [];
|
||||||
} else {
|
} else {
|
||||||
$this->seq->next();
|
$this->seq->next();
|
||||||
}
|
}
|
||||||
|
$this->current = ['valid' => $this->seq->valid(), 'key' => $this->seq->key(), 'value' => $this->seq->current()];
|
||||||
++$this->index0;
|
++$this->index0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rewind(): void
|
public function rewind(): void
|
||||||
{
|
{
|
||||||
$this->seq->rewind();
|
$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;
|
$this->index0 = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function valid(): bool
|
public function valid(): bool
|
||||||
{
|
{
|
||||||
return $this->seq->valid();
|
return $this->current['valid'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function iterated(): bool
|
public function iterated(): bool
|
||||||
@@ -89,11 +96,26 @@ final class Loop implements \Iterator
|
|||||||
|
|
||||||
public function isLast(): bool
|
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->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
|
$tests[] = [$node, <<<EOF
|
||||||
// line 1
|
// line 1
|
||||||
\$context['_parent'] = \$context;
|
\$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"]) {
|
foreach (\$__internal_compile_0 as \$context["key"] => \$context["item"]) {
|
||||||
yield {$this->getVariableGetter('foo')};
|
yield {$this->getVariableGetter('foo')};
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ EOF
|
|||||||
$tests[] = [$node, <<<EOF
|
$tests[] = [$node, <<<EOF
|
||||||
// line 1
|
// line 1
|
||||||
\$context['_parent'] = \$context;
|
\$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']);
|
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent']);
|
||||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||||
yield {$this->getVariableGetter('foo')};
|
yield {$this->getVariableGetter('foo')};
|
||||||
@@ -100,7 +100,7 @@ EOF
|
|||||||
$tests[] = [$node, <<<EOF
|
$tests[] = [$node, <<<EOF
|
||||||
// line 1
|
// line 1
|
||||||
\$context['_parent'] = \$context;
|
\$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']);
|
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent']);
|
||||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||||
yield {$this->getVariableGetter('foo')};
|
yield {$this->getVariableGetter('foo')};
|
||||||
@@ -122,7 +122,7 @@ EOF
|
|||||||
$tests[] = [$node, <<<EOF
|
$tests[] = [$node, <<<EOF
|
||||||
// line 1
|
// line 1
|
||||||
\$context['_parent'] = \$context;
|
\$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']);
|
\$context['loop'] = new \Twig\Runtime\LoopContext(\$__internal_compile_0, \$context['_parent']);
|
||||||
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
foreach (\$__internal_compile_0 as \$context["k"] => \$context["v"]) {
|
||||||
yield {$this->getVariableGetter('foo')};
|
yield {$this->getVariableGetter('foo')};
|
||||||
|
|||||||
Reference in New Issue
Block a user