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); 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() {