Skip to content

[BUGFIX] Handle generators and traversables correctly in ForViewHelper#1384

Open
Kanti wants to merge 1 commit into
TYPO3:mainfrom
Kanti:bugfix/iterator-total-for-viewhelper
Open

[BUGFIX] Handle generators and traversables correctly in ForViewHelper#1384
Kanti wants to merge 1 commit into
TYPO3:mainfrom
Kanti:bugfix/iterator-total-for-viewhelper

Conversation

@Kanti

@Kanti Kanti commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What changed

This PR improves how ForViewHelper handles generators and other traversables.

The ViewHelper sometimes has to look at all items before rendering them, for example when reverse="true" is used or when {iterator.total} is needed.
Until now, that was done with iterator_to_array().
That works for simple cases, but it loses entries when a generator yields the same key more than once.
The implementation now keeps materialized traversable items as key-value pairs instead.
That way, every yielded item is preserved, including duplicate keys.

Plain generator loops are still rendered directly whenever possible,
so we avoid consuming or buffering generators unless Fluid actually needs to know the full item list up front.

Why

PHP foreach renders every yielded generator item, even if keys are duplicated.
ForViewHelper should behave the same way.
Without this change, using iteration or reverse could silently drop entries from a valid generator.

Tests

This adds test coverage for:

  • non-countable traversables with {iterator.total}
  • plain generator rendering without look-ahead
  • generators with {iterator.total}
  • generators that yield duplicate keys
  • duplicate generator keys without exposing the key argument
  • empty generators with {iterator.total}
  • empty generators with reverse="true"
  • duplicate generator keys together with reverse="true"
  • generators with both reverse="true" and {iterator.total}
  • duplicate generator keys together with reverse="true" and {iterator.total}
  • countable traversables with iteration metadata

Comment thread src/ViewHelpers/ForViewHelper.php Outdated
@Kanti
Kanti force-pushed the bugfix/iterator-total-for-viewhelper branch 2 times, most recently from 3c55772 to 772a0d2 Compare July 1, 2026 12:34
@Kanti Kanti changed the title [BUGFIX] Fix ForViewHelper iteration total for traversables [BUGFIX] Preserve traversable entries in ForViewHelper Jul 1, 2026
@Kanti
Kanti force-pushed the bugfix/iterator-total-for-viewhelper branch from 772a0d2 to 2c0411e Compare July 1, 2026 12:47
@Kanti Kanti changed the title [BUGFIX] Preserve traversable entries in ForViewHelper [BUGFIX] Handle generators correctly in ForViewHelper Jul 2, 2026
@Kanti
Kanti force-pushed the bugfix/iterator-total-for-viewhelper branch from 2c0411e to cf316b1 Compare July 2, 2026 07:23
@Kanti Kanti changed the title [BUGFIX] Handle generators correctly in ForViewHelper [BUGFIX] Handle generators and traversables correctly in ForViewHelper Jul 2, 2026
Comment thread src/ViewHelpers/ForViewHelper.php Outdated
$this->arguments['each'] = array_reverse($this->arguments['each'], true);
} else {
$items = [];
foreach ($this->arguments['each'] as $keyValue => $singleElement) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not a fan of preemptively unwinding a Generator which may produce a lot of items. One point of generators is reducing memory usage which would be rendered useless here.

Still that's the behavior we always had so far so I guess it's acceptable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generator can only be read once and cannot be reversed unless we collect all the items and reverse them.
Therefore, there is no other possible solution here.

The same problem applies if we need the count for each of the elements.

Comment thread src/ViewHelpers/ForViewHelper.php Outdated
Comment thread src/ViewHelpers/ForViewHelper.php Outdated
}
}
if (isset($this->arguments['iteration'])) {
if ($items === null && !is_countable($this->arguments['each'])) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is rather hard to read and understand. We could instead always use $items as the result of a normalization from various input formats.

Also we completely skip Countable here. One can in fact implement a very memory friendly object which iterates using a generator and implement Countable. (Think e.g. SELECT * & COUNT(*) for DB data.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find a solution where we could always use $items while keeping the generators streaming the items in case iteration and reverse are not used.
This sadly gives us two different ways to iterate.

I will try to make it more readable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's more readable now. What do you think?

Materialize traversables as key-value pairs when reverse iteration or
iteration metadata requires looking ahead. This keeps duplicate yielded
keys intact and allows iteration totals to work for non-countable
traversables.

Add coverage for non-countable traversables, generators, duplicate keys,
and reverse rendering.
@Kanti
Kanti force-pushed the bugfix/iterator-total-for-viewhelper branch from cf316b1 to d9b986c Compare July 2, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants