Skip to content

[6.x] Give each Stache traversal its own filter - #15551

Merged
jasonvarga merged 1 commit into
statamic:6.xfrom
joshdaugherty:fix/stache-traverser-shared-filter
Sep 30, 2026
Merged

jasonvarga merged 1 commit into
statamic:6.xfrom
joshdaugherty:fix/stache-traverser-shared-filter

Conversation

@joshdaugherty

@joshdaugherty joshdaugherty commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #15550.
References #10410

Every Stache store shares one Traverser, because Store and TaxonomyTermsStore reach it through a real-time facade, whose resolved instance Laravel caches. filter() stored the callback on that shared instance, and traverse() reads it again for each file. So when a store's filter starts another traversal partway through the loop, which happens on multisite when CollectionEntriesStore::getItemFilter() resolves the collection against a cold cache, the running traversal carries on with the other store's filter. The entries store then keeps only its first file and caches that as its paths.

This makes filter() return a clone that carries the callback, so each traversal keeps its own filter. All three core call sites (Store.php:193, Store.php:309 and TaxonomyTermsStore.php:166) already chain filter()->traverse(), so they are unaffected.

Tests, both of which fail without the fix:

  • filtering_does_not_change_the_filter_of_the_original_instance applies two filters to one traverser and checks that each result has its own filter and the original has none.
  • a_traversal_started_by_a_filter_does_not_replace_the_running_traversals_filter sets up a multisite collection with three entries on a cold Stache and asserts that the entries store's paths contain all three. On 6.x it returns one.

Code that calls Traverser::filter() and Traverser::traverse() as two separate facade calls, relying on the filter persisting between them, would no longer be filtered. Core doesn't do this. If that's a concern, the alternatives are:

  • Pass the filter to traverse() as an optional argument and have the stores use that. This is the most explicit option, but it changes the method signature.
  • Stop using the real-time facade in the stores and create a Traverser for each traversal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016o2kMZ4DZaNGJ4pUqPov7D
@jasonvarga
jasonvarga merged commit e74f69b into statamic:6.x Sep 30, 2026
63 checks passed
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.

Traverser::filter() is shared across stores, so a nested traversal replaces the running one's filter

2 participants