From ddcc91547f618908303f8fdf308b0fb22b5b0e46 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 13 Nov 2026 04:50:21 +0000 Subject: [PATCH] Write an index once when several structured entries are updated Reordering a collection's tree, or changing a page that has children, updates the order, parent and uri indexes for each entry affected, and each update wrote the whole index. They are all put first now and the index is written once. An index class with its own updateItem() is still called for each. --- src/Stache/Indexes/Index.php | 29 +++++ src/Stache/Stores/CollectionEntriesStore.php | 5 +- .../StructuredEntryIndexUpdatesTest.php | 106 ++++++++++++++++++ 3 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 tests/Stache/Stores/StructuredEntryIndexUpdatesTest.php diff --git a/src/Stache/Indexes/Index.php b/src/Stache/Indexes/Index.php index 7e437258e70..f1ae2b2e856 100644 --- a/src/Stache/Indexes/Index.php +++ b/src/Stache/Indexes/Index.php @@ -116,7 +116,36 @@ public function updateItem($item) { $this->load(); + $this->putItem($item); + + $this->cache(); + } + + private function putItem($item) + { $this->put($this->store->getItemKey($item), $this->getItemValue($item)); + } + + /** + * Updates a number of items and writes the index once, where updating + * them one at a time would write the whole of it once for each. + */ + public function updateItems($items) + { + // An index with its own way of updating an item gets to use it. + if ((new \ReflectionMethod($this, 'updateItem'))->getDeclaringClass()->getName() !== self::class) { + collect($items)->each(fn ($item) => $this->updateItem($item)); + + return; + } + + if (collect($items)->isEmpty()) { + return; + } + + $this->load(); + + collect($items)->each(fn ($item) => $this->putItem($item)); $this->cache(); } diff --git a/src/Stache/Stores/CollectionEntriesStore.php b/src/Stache/Stores/CollectionEntriesStore.php index a5be77798da..c76d57b0023 100644 --- a/src/Stache/Stores/CollectionEntriesStore.php +++ b/src/Stache/Stores/CollectionEntriesStore.php @@ -414,10 +414,7 @@ private function updateEntriesWithinIndex($index, $ids) return $index->update(); } - collect($ids) - ->map(fn ($id) => Entry::find($id)) - ->filter() - ->each(fn ($entry) => $index->updateItem($entry)); + $index->updateItems(collect($ids)->map(fn ($id) => Entry::find($id))->filter()); } private function updateEntriesWithinStore($ids) diff --git a/tests/Stache/Stores/StructuredEntryIndexUpdatesTest.php b/tests/Stache/Stores/StructuredEntryIndexUpdatesTest.php new file mode 100644 index 00000000000..d4da1b0acd2 --- /dev/null +++ b/tests/Stache/Stores/StructuredEntryIndexUpdatesTest.php @@ -0,0 +1,106 @@ +key === "stache::indexes::entries::pages::$index") { + $writes++; + } + }); + + $callback(); + + return $writes; + } + + private function pages() + { + $collection = tap(Collection::make('pages')->routes('{parent_uri}/{slug}')->structureContents(['root' => false]))->save(); + + collect(['one', 'two', 'three'])->each(fn ($id) => EntryFactory::collection('pages')->id($id)->slug($id)->create()); + + return $collection; + } + + #[Test] + public function updating_the_order_of_several_entries_writes_the_index_once() + { + $collection = $this->pages(); + $store = Stache::store('entries')->store('pages'); + $store->index('order')->load(); + + $collection->structure()->in('en')->tree([['entry' => 'three'], ['entry' => 'one'], ['entry' => 'two']])->saveQuietly(); + + $writes = $this->writesTo('order', fn () => $store->updateOrders(['one', 'two', 'three'])); + + $this->assertSame(1, $writes); + $this->assertEquals(['one' => 2, 'two' => 3, 'three' => 1], $store->index('order')->items()->all()); + } + + #[Test] + public function updating_the_parents_and_uris_of_several_entries_writes_each_index_once() + { + $collection = $this->pages(); + $store = Stache::store('entries')->store('pages'); + $store->index('parent')->load(); + $store->index('uri')->load(); + + $collection->structure()->in('en')->tree([['entry' => 'one', 'children' => [['entry' => 'two'], ['entry' => 'three']]]])->saveQuietly(); + + $this->assertSame(1, $this->writesTo('parent', fn () => $store->updateParents(['two', 'three']))); + $this->assertSame(1, $this->writesTo('uri', fn () => $store->updateUris(['two', 'three']))); + + $this->assertEquals(['one' => null, 'two' => 'one', 'three' => 'one'], $store->index('parent')->items()->all()); + $this->assertEquals(['one' => '/one', 'two' => '/one/two', 'three' => '/one/three'], $store->index('uri')->items()->all()); + } + + #[Test] + public function ids_that_arent_entries_are_passed_over() + { + $this->pages(); + $store = Stache::store('entries')->store('pages'); + $before = $store->index('order')->load()->items()->all(); + + $this->assertSame(0, $this->writesTo('order', fn () => $store->updateOrders(['missing']))); + $this->assertEquals($before, $store->index('order')->items()->all()); + } + + #[Test] + public function an_index_with_its_own_way_of_updating_an_item_is_still_asked_to() + { + $this->pages(); + $store = Stache::store('entries')->store('pages'); + + $index = new class($store, 'title') extends Value + { + public array $updated = []; + + public function updateItem($item) + { + $this->updated[] = $item->id(); + } + }; + + $index->updateItems(collect(['one', 'two'])->map(fn ($id) => \Statamic\Facades\Entry::find($id))); + + $this->assertSame(['one', 'two'], $index->updated); + } +}