Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/ViewHelpers/ForViewHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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);
Expand Down
147 changes: 144 additions & 3 deletions tests/Functional/ViewHelpers/ForViewHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,147 @@ public static function renderDataProvider(): \Generator
. '</ul>',
];

$value = new class () implements \IteratorAggregate {
public function getIterator(): \Traversable
{
return new \ArrayIterator([
'first' => 'foo',
'second' => 'bar',
]);
}
};
yield 'iterator total works for non countable traversable' => [
'<f:for each="{value}" as="item" iteration="iterator">{item}: {iterator.total}, </f:for>',
['value' => $value],
'foo: 2, bar: 2, ',
];

yield 'generator gets traversed' => [
'<f:for each="{value}" as="item">{item} </f:for>',
static function () {
$value = (static function (): \Generator {
yield 'first' => 'foo';
yield 'second' => 'bar';
})();
return ['value' => $value];
},
'foo bar ',
];

yield 'iterator total works for generator' => [
'<f:for each="{value}" as="item" iteration="iterator">{item}: {iterator.total}, </f:for>',
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' => [
'<f:for each="{value}" as="item" iteration="iterator">{item}{f:if(condition: iterator.isLast, then: \'!\', else: \', \')}</f:for>',
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' => [
'<f:for each="{value}" as="item">{item} </f:for>',
static function () {
$value = (static function (): \Generator {
yield 'duplicate' => 'foo';
yield 'duplicate' => 'bar';
})();
return ['value' => $value];
},
'foo bar ',
];

yield 'iterator preserves duplicate keys from generator' => [
'<f:for each="{value}" as="item" key="key" iteration="iterator">{key}: {item} / {iterator.total}, </f:for>',
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' => [
'<f:for each="{value}" as="item" iteration="iterator">{item}: {iterator.total}, </f:for>',
static function () {
$value = (static function (): \Generator {
yield from [];
})();
return ['value' => $value];
},
'',
];

yield 'empty generator with reverse returns empty string' => [
'<f:for each="{value}" as="item" reverse="true">{item} </f:for>',
static function () {
$value = (static function (): \Generator {
yield from [];
})();
return ['value' => $value];
},
'',
];

yield 'reverse preserves duplicate keys from generator' => [
'<f:for each="{value}" as="item" key="key" reverse="true">{key}: {item}, </f:for>',
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' => [
'<f:for each="{value}" as="item" reverse="true" iteration="iterator">{item}: {iterator.total} / {iterator.isFirst} / {iterator.isLast}, </f:for>',
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' => [
'<f:for each="{value}" as="item" key="key" reverse="true" iteration="iterator">{key}: {item} / {iterator.total}, </f:for>',
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' => [
'<f:for each="{value}" as="item" iteration="iterator">{item}: {iterator.total} / {iterator.isFirst} / {iterator.isLast}, </f:for>',
['value' => $value],
'foo: 2 / 1 / , bar: 2 / / 1, ',
];

$value = ['item'];
yield 'iterator not available if not requested' => [
'<f:for each="{value}" as="item">Total: {iterator.total}</f:for>',
Expand Down Expand Up @@ -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());
Expand Down
Loading