Repository navigation
Traverser::filter() is shared across stores, so a nested traversal replaces the running one's filter #15550
Description
Bug description
On a multisite install with a cold Stache cache, the first rebuild of a collection's entries store can keep only the first entry file and drop the rest. The truncated list is then cached as the store's paths, so the missing entries stay missing (404s, empty listings) until the Stache is cleared again.
The cause is that every store shares one Traverser. Store and TaxonomyTermsStore reach it through a real-time facade (use Facades\Statamic\Stache\Traverser), and Laravel caches a facade's resolved instance, so every call in a request gets the same object. Traverser::filter() stores the callback on that object, and traverse() reads $this->filter again for each file inside its loop.
On multisite, CollectionEntriesStore::getItemFilter() calls $this->collection()->sites(), which resolves the collection. On a cold cache that builds the collections store, which calls Traverser::filter([$this, 'getItemFilter']) on the same shared instance. When control returns, the entries traversal carries on with CollectionsStore::getItemFilter as its filter, and that filter rejects the remaining .md files.
Measured on a real site with 82 entry files in pages: the first traversal kept 1 and the collections store's filter rejected the other 81. A second traversal, with the collections store already built, kept all 82. Both passes used the same traverser instance.
Still present in v6.34.1 and on 6.x (1ae088399).
How to reproduce
- Enable multisite with two sites,
enandfr. - Create a collection
blogwithsites: [en, fr]and three entries incontent/collections/blog/en/. - Clear the Stache (
php please stache:clear). - Read the collection's entries store, for example
Stache::store('entries')->store('blog')->paths(), or list the collection's entries.
Only the first entry file is returned. #15551 adds a test that reproduces this in Statamic's own suite; on 6.x it fails with Failed asserting that actual size 1 matches expected size 3.
Logs
No error is thrown; the entries are silently missing.Environment
Environment
Laravel Version: 13.24.0
PHP Version: 8.4.23
Composer Version: 2.10.2
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: en
Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: NOT CACHED
Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: smtp
Queue: sync
Session: file
Storage
public/storage: NOT LINKED
Statamic
Addons: 9
License Key: Not set
Sites: 255 (names redacted)
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.26.0 PRO
Statamic Addons
el-schneider/statamic-admin-bar: 0.3.2
jacksleight/statamic-bard-mutator: 3.0.5
jacksleight/statamic-bard-texstyle: 4.1.1
mitydigital/sitemapamic: 3.5.1
statamic/eloquent-driver: 5.10.0
statamic/ssg: 4.1.0
stillat/relationships: 2.4.0
studio1902/statamic-peak-tools: 9.2.0
uams-web/wordpress-importer: 0.23.1
Statamic Eloquent Driver
Addon Settings: file
Asset Containers: file
Assets: eloquent
Blueprints: file
Collection Trees: file
Collections: file
Entries: file
Fieldsets: file
Form Submissions: file
Forms: file
Global Sets: file
Global Variables: file
Navigation Trees: file
Navigations: file
Revisions: file
Sites: file
Taxonomies: file
Terms: file
Tokens: fileInstallation
Existing Laravel app
Additional details
The environment above runs 6.26.0 with a local patch for this bug. The reproduction and the fix were checked against v6.34.1 and 6.x.
This may explain some of the reports in #10410. Several people there say the fix is to run stache:warm twice, and a second rebuild is exactly what recovers here, but that hasn't been confirmed against those sites.
The fix is to have filter() return a clone carrying the callback instead of changing the shared instance. All three core call sites (Store.php:193, Store.php:309 and TaxonomyTermsStore.php:166) already chain filter()->traverse(), so none of them depends on the old behaviour.
Activity