From c4c16b28191c5cf17a5f460cc62ad45b42e59b75 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 6 Nov 2026 01:41:17 +0000 Subject: [PATCH 1/2] Add a test for entry URIs after a tree file changes on disk This fails on 6.x: moving a page in a collection tree file leaves the entry's URI as it was until the Stache is cleared. --- tests/Stache/FeatureTest.php | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/Stache/FeatureTest.php b/tests/Stache/FeatureTest.php index 04e02bf89f2..809834eb729 100644 --- a/tests/Stache/FeatureTest.php +++ b/tests/Stache/FeatureTest.php @@ -146,6 +146,62 @@ public function it_gets_an_entry_in_structure_by_uri() $this->assertEquals('Directors', $entry->title()); } + #[Test] + public function the_watcher_updates_entry_uris_when_a_tree_file_changes() + { + config(['statamic.stache.watcher' => true]); + + $path = __DIR__.'/__fixtures__/content/structures/collections/pages.yaml'; + $original = file_get_contents($path); + + try { + $this->assertEquals('/about/board/directors', Entry::find('pages-directors')->uri()); + + // Move "directors" from under about/board to the top level, as a git pull or a hand edit would. + file_put_contents($path, <<<'YAML' +tree: + - + entry: pages-home + - + entry: pages-about + children: + - + entry: pages-board + - + entry: pages-directors + - + entry: pages-blog + +YAML); + touch($path, time() + 10); + + $this->nextRequest(); + + $this->assertEquals('/directors', Entry::find('pages-directors')->uri()); + $this->assertEquals('pages-directors', Entry::findByUri('/directors')?->id()); + $this->assertNull(Entry::findByUri('/about/board/directors')); + } finally { + file_put_contents($path, $original); + } + } + + /** + * What the next request starts with: the cache as it is, nothing memoized in the stores. + */ + private function nextRequest() + { + \Statamic\Facades\Blink::flush(); + + $this->stache->stores() + ->flatMap(fn ($store) => $store instanceof \Statamic\Stache\Stores\AggregateStore ? $store->discoverStores() : [$store]) + ->each(function ($store) { + $store->resetMemoizedState(); + (fn () => $this->fileChangesHandled = false)->call($store); + }); + + app('stache.indexes')->each->resetMemoizedState(); + } + #[Test] public function it_returns_null_when_cannot_find_entry_by_uri() { From be71615ec7aa97b0df5d542aef7f271e4ed11812 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 6 Nov 2026 01:41:17 +0000 Subject: [PATCH 2/2] Update entries when the watcher picks up a changed tree file Saving a tree through the API updates the entries it affects in the CollectionTreeSaved listeners. When a tree file changes on disk instead, the watcher re-reads the tree but nothing updates the entries, so their URIs (and order) stay stale. CollectionTreeStore now updates the collection's entry parents, order and URIs when the watcher picks up a changed tree, under the same conditions as UpdateStructuredEntryUris. Structured collections' entry stores check the trees for changes before their own indexes are used, since that's where their URIs come from. Fixes #15796 --- src/Stache/Stores/CollectionEntriesStore.php | 12 +++++++++++ src/Stache/Stores/CollectionTreeStore.php | 22 ++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Stache/Stores/CollectionEntriesStore.php b/src/Stache/Stores/CollectionEntriesStore.php index 09109f073da..91546bb2d98 100644 --- a/src/Stache/Stores/CollectionEntriesStore.php +++ b/src/Stache/Stores/CollectionEntriesStore.php @@ -26,6 +26,18 @@ class CollectionEntriesStore extends ChildStore protected $collection; private bool $shouldBlinkEntryUris = true; + public function handleFileChanges() + { + // Entries get their uris, parents and order from the collection trees, so a tree file that + // changed on disk has to be picked up before this store's indexes are used. (Each store + // only checks once per request, so this can't loop.) + if (Stache::isWatcherEnabled() && ! $this->fileChangesHandled && $this->collection()?->hasStructure()) { + Stache::store('collection-trees')?->handleFileChanges(); + } + + parent::handleFileChanges(); + } + protected function collection() { return $this->collection ?? Collection::findByHandle($this->childKey); diff --git a/src/Stache/Stores/CollectionTreeStore.php b/src/Stache/Stores/CollectionTreeStore.php index 1caac172c08..0e415af77bb 100644 --- a/src/Stache/Stores/CollectionTreeStore.php +++ b/src/Stache/Stores/CollectionTreeStore.php @@ -29,6 +29,28 @@ public function getItemFilter(SplFileInfo $file) return $collection->hasStructure(); } + /** + * A tree file changed on disk (a git pull, a deploy, a hand edit) and the watcher picked it up. + * Saving a tree through the API updates the entries it affects in the CollectionTreeSaved + * listeners, but there's no saved event here, so update the collection's entries directly. + * The previous tree is already gone from the cache, so there's no diff to narrow this down. + */ + protected function handleModifiedItem($tree) + { + if (! $collection = $tree->collection()) { + return; + } + + $collection->updateEntryParent(); + $collection->updateEntryOrder(); + + // Same conditions as UpdateStructuredEntryUris: a single-depth (orderable) structure + // never affects uris, and without a route there are none. + if (! $collection->orderable() && $collection->route($tree->locale())) { + $collection->updateEntryUris(); + } + } + protected function newTreeClassByPath($path) { [$site, $handle] = $this->parseTreePath($path);