diff --git a/src/ViewHelpers/ForViewHelper.php b/src/ViewHelpers/ForViewHelper.php
index f6d336697..8f3da82ec 100644
--- a/src/ViewHelpers/ForViewHelper.php
+++ b/src/ViewHelpers/ForViewHelper.php
@@ -99,8 +99,18 @@ public function render(): string
if (is_object($this->arguments['each']) && !$this->arguments['each'] instanceof \Traversable) {
throw new InvalidArgumentValueException('ForViewHelper only supports arrays and objects implementing \Traversable interface', 1248728393);
}
+ $isKeyValuePair = false;
+ if ($this->arguments['reverse'] === true || (isset($this->arguments['iteration'])) && !is_countable($this->arguments['each'])) {
+ $isKeyValuePair = true;
+ $traversable = $this->arguments['each'];
+ $this->arguments['each'] = [];
+ foreach ($traversable as $key => $singleElement) {
+ // unpack the traversable, but keep duplicate keys.
+ $this->arguments['each'][] = [$key, $singleElement];
+ }
+ }
if ($this->arguments['reverse'] === true) {
- $this->arguments['each'] = array_reverse(iterator_to_array($this->arguments['each']), true);
+ $this->arguments['each'] = array_reverse($this->arguments['each'], true);
}
if (isset($this->arguments['iteration'])) {
$iterationData = [
@@ -114,6 +124,9 @@ public function render(): string
$this->renderingContext->setVariableProvider(new ScopedVariableProvider($globalVariableProvider, $localVariableProvider));
$output = '';
foreach ($this->arguments['each'] as $keyValue => $singleElement) {
+ if ($isKeyValuePair === true) {
+ [$keyValue, $singleElement] = $singleElement;
+ }
$localVariableProvider->add($this->arguments['as'], $singleElement);
if (isset($this->arguments['key'])) {
$localVariableProvider->add($this->arguments['key'], $keyValue);
diff --git a/tests/Functional/ViewHelpers/ForViewHelperTest.php b/tests/Functional/ViewHelpers/ForViewHelperTest.php
index 741de873b..c1c1a3ddf 100644
--- a/tests/Functional/ViewHelpers/ForViewHelperTest.php
+++ b/tests/Functional/ViewHelpers/ForViewHelperTest.php
@@ -107,6 +107,147 @@ public static function renderDataProvider(): \Generator
. '',
];
+ $value = new class () implements \IteratorAggregate {
+ public function getIterator(): \Traversable
+ {
+ return new \ArrayIterator([
+ 'first' => 'foo',
+ 'second' => 'bar',
+ ]);
+ }
+ };
+ yield 'iterator total works for non countable traversable' => [
+ '{item}: {iterator.total}, ',
+ ['value' => $value],
+ 'foo: 2, bar: 2, ',
+ ];
+
+ yield 'generator gets traversed' => [
+ '{item} ',
+ static function () {
+ $value = (static function (): \Generator {
+ yield 'first' => 'foo';
+ yield 'second' => 'bar';
+ })();
+ return ['value' => $value];
+ },
+ 'foo bar ',
+ ];
+
+ yield 'iterator total works for generator' => [
+ '{item}: {iterator.total}, ',
+ static function () {
+ $value = (static function (): \Generator {
+ yield 'first' => 'foo';
+ yield 'second' => 'bar';
+ })();
+ return ['value' => $value];
+ },
+ 'foo: 2, bar: 2, ',
+ ];
+
+ yield 'iterator last works for generator' => [
+ '{item}{f:if(condition: iterator.isLast, then: \'!\', else: \', \')}',
+ static function () {
+ $value = (static function (): \Generator {
+ yield 'Lucas';
+ yield 'Helene';
+ yield 'Thomas';
+ })();
+ return ['value' => $value];
+ },
+ 'Lucas, Helene, Thomas!',
+ ];
+
+ yield 'generator preserves duplicate keys without key argument' => [
+ '{item} ',
+ static function () {
+ $value = (static function (): \Generator {
+ yield 'duplicate' => 'foo';
+ yield 'duplicate' => 'bar';
+ })();
+ return ['value' => $value];
+ },
+ 'foo bar ',
+ ];
+
+ yield 'iterator preserves duplicate keys from generator' => [
+ '{key}: {item} / {iterator.total}, ',
+ static function () {
+ $value = (static function (): \Generator {
+ yield 'duplicate' => 'foo';
+ yield 'duplicate' => 'bar';
+ })();
+ return ['value' => $value];
+ },
+ 'duplicate: foo / 2, duplicate: bar / 2, ',
+ ];
+
+ yield 'empty generator with iteration returns empty string' => [
+ '{item}: {iterator.total}, ',
+ static function () {
+ $value = (static function (): \Generator {
+ yield from [];
+ })();
+ return ['value' => $value];
+ },
+ '',
+ ];
+
+ yield 'empty generator with reverse returns empty string' => [
+ '{item} ',
+ static function () {
+ $value = (static function (): \Generator {
+ yield from [];
+ })();
+ return ['value' => $value];
+ },
+ '',
+ ];
+
+ yield 'reverse preserves duplicate keys from generator' => [
+ '{key}: {item}, ',
+ static function () {
+ $value = (static function (): \Generator {
+ yield 'duplicate' => 'foo';
+ yield 'duplicate' => 'bar';
+ })();
+ return ['value' => $value];
+ },
+ 'duplicate: bar, duplicate: foo, ',
+ ];
+
+ yield 'reverse and iteration work for generator' => [
+ '{item}: {iterator.total} / {iterator.isFirst} / {iterator.isLast}, ',
+ static function () {
+ $value = (static function (): \Generator {
+ yield 'first' => 'foo';
+ yield 'second' => 'bar';
+ })();
+ return ['value' => $value];
+ },
+ 'bar: 2 / 1 / , foo: 2 / / 1, ',
+ ];
+
+ yield 'reverse and iteration preserve duplicate keys from generator' => [
+ '{key}: {item} / {iterator.total}, ',
+ static function () {
+ $value = (static function (): \Generator {
+ yield 'duplicate' => 'foo';
+ yield 'duplicate' => 'bar';
+ })();
+ return ['value' => $value];
+ },
+ 'duplicate: bar / 2, duplicate: foo / 2, ',
+ ];
+
+ $value = new \ArrayObject(['foo', 'bar']);
+ yield 'iterator metadata works for countable traversable' => [
+ '{item}: {iterator.total} / {iterator.isFirst} / {iterator.isLast}, ',
+ ['value' => $value],
+ 'foo: 2 / 1 / , bar: 2 / / 1, ',
+ ];
+
$value = ['item'];
yield 'iterator not available if not requested' => [
'Total: {iterator.total}',
@@ -200,16 +341,16 @@ public static function renderDataProvider(): \Generator
#[DataProvider('renderDataProvider')]
#[Test]
- public function render(string $template, array $variables, string $expected): void
+ public function render(string $template, array|callable $variables, string $expected): void
{
$view = new TemplateView();
- $view->assignMultiple($variables);
+ $view->assignMultiple(is_callable($variables) ? $variables() : $variables);
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($template);
self::assertSame($expected, $view->render());
$view = new TemplateView();
- $view->assignMultiple($variables);
+ $view->assignMultiple(is_callable($variables) ? $variables() : $variables);
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($template);
self::assertSame($expected, $view->render());