diff --git a/composer.json b/composer.json index 5b134ff9..c907323e 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ }, "require": { "php": "^8.1", - "statamic/cms": "^5.12" + "statamic/cms": "dev-collections-doing-stuff-entries-should-probably-be-doing as 5.12.0" }, "require-dev": { "doctrine/dbal": "^3.8", diff --git a/src/Collections/CollectionRepository.php b/src/Collections/CollectionRepository.php index ccb1f7a0..136ca2e0 100644 --- a/src/Collections/CollectionRepository.php +++ b/src/Collections/CollectionRepository.php @@ -4,22 +4,11 @@ use Illuminate\Support\Collection as IlluminateCollection; use Statamic\Contracts\Entries\Collection as CollectionContract; -use Statamic\Eloquent\Jobs\UpdateCollectionEntryOrder; use Statamic\Facades\Blink; use Statamic\Stache\Repositories\CollectionRepository as StacheRepository; class CollectionRepository extends StacheRepository { - public function updateEntryUris($collection, $ids = null) - { - $query = $collection->queryEntries() - ->when($ids, fn ($query) => $query->whereIn('id', $ids)) - ->get() - ->each(function ($entry) { - app('statamic.eloquent.entries.model')::find($entry->id())->update(['uri' => $entry->uri()]); - }); - } - public function all(): IlluminateCollection { return Blink::once('eloquent-collections', function () { @@ -76,22 +65,4 @@ public static function bindings(): array CollectionContract::class => Collection::class, ]; } - - public function updateEntryOrder(CollectionContract $collection, $ids = null) - { - $query = $collection->queryEntries() - ->when($ids, fn ($query) => $query->whereIn('id', $ids)) - ->get(['id']) - ->each(function ($entry) { - $dispatch = UpdateCollectionEntryOrder::dispatch($entry->id()); - - $connection = config('statamic.eloquent-driver.collections.update_entry_order_connection', 'default'); - - if ($connection != 'default') { - $dispatch->onConnection($connection); - } - - $dispatch->onQueue(config('statamic.eloquent-driver.collections.update_entry_order_queue', 'default')); - }); - } } diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 0a1e6100..65f5ce3d 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -100,7 +100,17 @@ public static function makeModelFromContract(EntryContract $source) $dataMappings = (new self)->getDataColumnMappings(new $class); + $attributes = []; + + if ($id = $source->id()) { + $attributes['id'] = $id; + + // Ensure that when calling $source->uri() that it doesn't use the cached value. + Blink::store('entry-uris')->forget($source->id()); + } + $attributes = [ + ...$attributes, 'origin_id' => $origin?->id(), 'site' => $source->locale(), 'slug' => $source->slug(), @@ -118,10 +128,6 @@ public static function makeModelFromContract(EntryContract $source) $attributes[$key] = $data->get($key); } - if ($id = $source->id()) { - $attributes['id'] = $id; - } - return $class::findOrNew($id)->fill($attributes); } diff --git a/src/Entries/EntryRepository.php b/src/Entries/EntryRepository.php index 15e8ebeb..4a3e00c6 100644 --- a/src/Entries/EntryRepository.php +++ b/src/Entries/EntryRepository.php @@ -4,6 +4,8 @@ use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Contracts\Entries\QueryBuilder; +use Statamic\Eloquent\Jobs\UpdateCollectionEntryOrder; +use Statamic\Eloquent\Jobs\UpdateCollectionEntryParent; use Statamic\Facades\Blink; use Statamic\Stache\Repositories\EntryRepository as StacheRepository; @@ -67,4 +69,50 @@ public function delete($entry) $entry->model()->delete(); } + + public function updateUris($collection, $ids = null) + { + $ids = collect($ids); + + $collection->queryEntries() + ->when($ids->isNotEmpty(), fn ($query) => $query->whereIn('id', $ids)) + ->get() + ->each(fn ($entry) => $entry->model()->update(['uri' => $entry->uri()])); + } + + public function updateOrders($collection, $ids = null) + { + $collection->queryEntries() + ->when($ids, fn ($query) => $query->whereIn('id', $ids)) + ->get(['id']) + ->each(function ($entry) { + $dispatch = UpdateCollectionEntryOrder::dispatch($entry->id()); + + $connection = config('statamic.eloquent-driver.collections.update_entry_order_connection', 'default'); + + if ($connection != 'default') { + $dispatch->onConnection($connection); + } + + $dispatch->onQueue(config('statamic.eloquent-driver.collections.update_entry_order_queue', 'default')); + }); + } + + public function updateParents($collection, $ids = null) + { + $collection->queryEntries() + ->when($ids, fn ($query) => $query->whereIn('id', $ids)) + ->get(['id']) + ->each(function ($entry) { + $dispatch = UpdateCollectionEntryParent::dispatch($entry->id()); + + $connection = config('statamic.eloquent-driver.collections.update_entry_parent_connection', 'default'); + + if ($connection != 'default') { + $dispatch->onConnection($connection); + } + + $dispatch->onQueue(config('statamic.eloquent-driver.collections.update_entry_parent_queue', 'default')); + }); + } } diff --git a/src/Jobs/UpdateCollectionEntryParent.php b/src/Jobs/UpdateCollectionEntryParent.php new file mode 100644 index 00000000..fcd3472c --- /dev/null +++ b/src/Jobs/UpdateCollectionEntryParent.php @@ -0,0 +1,28 @@ +entryId = $entryId; + } + + public function handle() + { + if ($entry = Entry::find($this->entryId)) { + $entry->save(); + } + } +} diff --git a/src/Listeners/UpdateStructuredEntryOrder.php b/src/Listeners/UpdateStructuredEntryOrder.php deleted file mode 100644 index 65c2d557..00000000 --- a/src/Listeners/UpdateStructuredEntryOrder.php +++ /dev/null @@ -1,35 +0,0 @@ -tree; - $collection = $tree->collection(); - - if (config('statamic.eloquent-driver.entries.driver', 'file') !== 'eloquent') { - return; - } - - if (config('statamic.eloquent-driver.collections.driver') === 'eloquent') { - // If the collections are configured to use Eloquent, then the entry - // order will be updated through the regular event/listener flow. - return; - } - - $diff = $tree->diff(); - - $ids = array_merge($diff->moved(), $diff->added()); - - if (empty($ids)) { - return; - } - - app(CollectionRepository::class)->updateEntryOrder($collection, $ids); - } -} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index c2e8a806..dbc4a73c 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -31,7 +31,6 @@ use Statamic\Eloquent\Forms\SubmissionRepository; use Statamic\Eloquent\Globals\GlobalRepository; use Statamic\Eloquent\Globals\GlobalVariablesRepository; -use Statamic\Eloquent\Listeners\UpdateStructuredEntryOrder; use Statamic\Eloquent\Revisions\RevisionRepository; use Statamic\Eloquent\Structures\CollectionTreeRepository; use Statamic\Eloquent\Structures\NavigationRepository; @@ -40,7 +39,6 @@ use Statamic\Eloquent\Taxonomies\TermQueryBuilder; use Statamic\Eloquent\Taxonomies\TermRepository; use Statamic\Eloquent\Tokens\TokenRepository; -use Statamic\Events\CollectionTreeSaved; use Statamic\Providers\AddonServiceProvider; use Statamic\Statamic; @@ -61,12 +59,6 @@ class ServiceProvider extends AddonServiceProvider \Statamic\Eloquent\Updates\ChangeFormSubmissionsIdType::class, ]; - protected $listen = [ - CollectionTreeSaved::class => [ - UpdateStructuredEntryOrder::class, - ], - ]; - public function boot() { parent::boot(); diff --git a/tests/Entries/EntryRepositoryTest.php b/tests/Entries/EntryRepositoryTest.php new file mode 100644 index 00000000..c874376b --- /dev/null +++ b/tests/Entries/EntryRepositoryTest.php @@ -0,0 +1,237 @@ +routes('blog/{slug}')->save(); + + (new Entry)->id(1)->collection($collection)->slug('alfa')->save(); + (new Entry)->id(2)->collection($collection)->slug('bravo')->save(); + (new Entry)->id(3)->collection($collection)->slug('charlie')->save(); + + $collection->routes('posts/{slug}')->save(); + + // Assert that the URIs are unchanged, to make sure that saving + // the collection isn't what caused the URIs to be updated. + $this->assertEquals([ + '/blog/alfa', + '/blog/bravo', + '/blog/charlie', + ], EntryModel::all()->map->uri->all()); + + (new EntryRepository(new Stache))->updateUris($collection); + + $this->assertEquals([ + '/posts/alfa', + '/posts/bravo', + '/posts/charlie', + ], EntryModel::all()->map->uri->all()); + } + + #[Test] + public function it_updates_the_uris_of_specific_entries_in_a_collection() + { + $collection = Collection::make('blog')->routes('blog/{slug}')->save(); + + (new Entry)->id(1)->collection($collection)->slug('alfa')->save(); + (new Entry)->id(2)->collection($collection)->slug('bravo')->save(); + (new Entry)->id(3)->collection($collection)->slug('charlie')->save(); + + $collection->routes('posts/{slug}')->save(); + + // Assert that the URIs are unchanged, to make sure that saving + // the collection isn't what caused the URIs to be updated. + $this->assertEquals([ + '/blog/alfa', + '/blog/bravo', + '/blog/charlie', + ], EntryModel::all()->map->uri->all()); + + (new EntryRepository(new Stache))->updateUris($collection, [2, 3]); + + $this->assertEquals([ + '/blog/alfa', + '/posts/bravo', + '/posts/charlie', + ], EntryModel::all()->map->uri->all()); + } + + #[Test] + public function it_updates_the_order_of_all_entries_in_a_collection() + { + Event::fake(CollectionTreeSaved::class); + + $collection = Collection::make('blog') + ->structureContents(['max_depth' => 1]) + ->save(); + + (new Entry)->id(1)->collection($collection)->slug('alfa')->save(); + (new Entry)->id(2)->collection($collection)->slug('bravo')->save(); + (new Entry)->id(3)->collection($collection)->slug('charlie')->save(); + (new Entry)->id(4)->collection($collection)->slug('delta')->save(); + + $collection->structure()->in('en')->tree([ + ['entry' => 4], + ['entry' => 2], + ['entry' => 1], + ['entry' => 3], + ])->save(); + + // Assert that the order is unchanged, to make sure that saving + // the structure isn't what caused the order to be updated. + $this->assertEquals([ + 1 => 1, + 2 => 1, + 3 => 1, + 4 => 1, + ], EntryModel::all()->mapWithKeys(fn ($e) => [$e->id => $e->order])->all()); + + (new EntryRepository(new Stache))->updateOrders($collection); + + $this->assertEquals([ + 1 => 3, + 2 => 2, + 3 => 4, + 4 => 1, + ], EntryModel::all()->mapWithKeys(fn ($e) => [$e->id => $e->order])->all()); + } + + #[Test] + public function it_updates_the_order_of_specific_entries_in_a_collection() + { + Event::fake(CollectionTreeSaved::class); + + $collection = Collection::make('blog') + ->structureContents(['max_depth' => 1]) + ->save(); + + (new Entry)->id(1)->collection($collection)->slug('alfa')->save(); + (new Entry)->id(2)->collection($collection)->slug('bravo')->save(); + (new Entry)->id(3)->collection($collection)->slug('charlie')->save(); + (new Entry)->id(4)->collection($collection)->slug('delta')->save(); + + $collection->structure()->in('en')->tree([ + ['entry' => 4], + ['entry' => 2], + ['entry' => 1], + ['entry' => 3], + ])->save(); + + // Assert that the order is unchanged, to make sure that saving + // the structure isn't what caused the order to be updated. + $this->assertEquals([ + 1 => 1, + 2 => 1, + 3 => 1, + 4 => 1, + ], EntryModel::all()->mapWithKeys(fn ($e) => [$e->id => $e->order])->all()); + + (new EntryRepository(new Stache))->updateOrders($collection, [2, 3]); + + $this->assertEquals([ + 1 => 1, + 2 => 2, + 3 => 4, + 4 => 1, + ], EntryModel::all()->mapWithKeys(fn ($e) => [$e->id => $e->order])->all()); + } + + #[Test] + public function it_updates_the_parents_of_all_entries_in_a_collection() + { + Event::fake(CollectionTreeSaved::class); + + $collection = Collection::make('blog') + ->structureContents(['root' => true]) + ->save(); + + (new Entry)->id(1)->collection($collection)->slug('alfa')->save(); + (new Entry)->id(2)->collection($collection)->slug('bravo')->save(); + (new Entry)->id(3)->collection($collection)->slug('charlie')->save(); + (new Entry)->id(4)->collection($collection)->slug('delta')->save(); + + $collection->structure()->in('en')->tree([ + ['entry' => 4], + ['entry' => 2, 'children' => [ + ['entry' => 1], + ]], + ['entry' => 3], + ])->save(); + + // Assert that the parents are unchanged, to make sure that saving + // the structure isn't what caused the parents to be updated. + $this->assertEquals([ + 1 => null, + 2 => null, + 3 => null, + 4 => null, + ], EntryModel::all()->mapWithKeys(fn ($e) => [$e->id => $e->data['parent'] ?? null])->all()); + + (new EntryRepository(new Stache))->updateParents($collection); + + $this->assertEquals([ + 1 => 2, + 2 => 4, + 3 => 4, + 4 => null, + ], EntryModel::all()->mapWithKeys(fn ($e) => [$e->id => $e->data['parent'] ?? null])->all()); + } + + #[Test] + public function it_updates_the_parents_of_specific_entries_in_a_collection() + { + Event::fake(CollectionTreeSaved::class); + + $collection = Collection::make('blog') + ->structureContents(['root' => true]) + ->save(); + + (new Entry)->id(1)->collection($collection)->slug('alfa')->save(); + (new Entry)->id(2)->collection($collection)->slug('bravo')->save(); + (new Entry)->id(3)->collection($collection)->slug('charlie')->save(); + (new Entry)->id(4)->collection($collection)->slug('delta')->save(); + + $collection->structure()->in('en')->tree([ + ['entry' => 4], + ['entry' => 2, 'children' => [ + ['entry' => 1], + ]], + ['entry' => 3], + ])->save(); + + // Assert that the parents are unchanged, to make sure that saving + // the structure isn't what caused the parents to be updated. + $this->assertEquals([ + 1 => null, + 2 => null, + 3 => null, + 4 => null, + ], EntryModel::all()->mapWithKeys(fn ($e) => [$e->id => $e->data['parent'] ?? null])->all()); + + (new EntryRepository(new Stache))->updateParents($collection, [2, 3]); + + $this->assertEquals([ + 1 => null, + 2 => 4, + 3 => 4, + 4 => null, + ], EntryModel::all()->mapWithKeys(fn ($e) => [$e->id => $e->data['parent'] ?? null])->all()); + } +} diff --git a/tests/Entries/EntryTest.php b/tests/Entries/EntryTest.php index be603b10..84edfbaf 100644 --- a/tests/Entries/EntryTest.php +++ b/tests/Entries/EntryTest.php @@ -301,4 +301,31 @@ public function it_doesnt_store_mapped_data_when_config_is_disabled() $this->assertSame($entry->foo, $fresh->foo); } + + #[Test] + public function saving_an_entry_updates_the_uri() + { + // The URI is stored in the Blink cache. This test ensures + // that the new URI is stored and not the blinked one. + + Collection::make('blog')->title('blog') + ->routes('{parent_uri}/{slug}') + ->save(); + + $entry = (new Entry()) + ->id('1.0') + ->collection('blog') + ->slug('the-slug') + ->data(['foo' => 'bar']); + + $entry->save(); + + $this->assertSame('/the-slug', $entry->uri()); + $this->assertSame('/the-slug', $entry->model()->uri); + + $entry->slug('the-new-slug')->save(); + + $this->assertSame('/the-new-slug', $entry->uri()); + $this->assertSame('/the-new-slug', $entry->model()->uri); + } }