diff --git a/src/Entries/EntryRepository.php b/src/Entries/EntryRepository.php index 4a3e00c6..a4346ac0 100644 --- a/src/Entries/EntryRepository.php +++ b/src/Entries/EntryRepository.php @@ -4,8 +4,10 @@ use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Contracts\Entries\QueryBuilder; +use Statamic\Eloquent\Events\TypeRetrieved; use Statamic\Eloquent\Jobs\UpdateCollectionEntryOrder; use Statamic\Eloquent\Jobs\UpdateCollectionEntryParent; +use Statamic\Entries\EntryCollection; use Statamic\Facades\Blink; use Statamic\Stache\Repositories\EntryRepository as StacheRepository; @@ -32,7 +34,9 @@ public function find($id): ?EntryContract return null; } - return $this->substitutionsById[$item->id()] ?? $item; + return tap($this->substitutionsById[$item->id()] ?? $item, function ($entry) { + TypeRetrieved::dispatch($entry); + }); } public function findByUri(string $uri, ?string $site = null): ?EntryContract @@ -48,7 +52,41 @@ public function findByUri(string $uri, ?string $site = null): ?EntryContract return null; } - return $this->substitutionsById[$item->id()] ?? $item; + return tap($this->substitutionsById[$item->id()] ?? $item, function ($entry) { + TypeRetrieved::dispatch($entry); + }); + } + + public function findByIds($ids): EntryCollection + { + $cached = collect($ids)->flip()->map(fn ($_, $id) => Blink::get("eloquent-entry-{$id}")); + $missingIds = $cached->reject()->keys(); + + $missingById = $this->query() + ->whereIn('id', $missingIds) + ->get() + ->keyBy->id(); + + $missingById->each(function ($entry, $id) { + Blink::put("eloquent-entry-{$id}", $entry); + }); + + $items = $cached + ->map(fn ($entry, $id) => $entry ?? $missingById->get($id)) + ->filter() + ->values(); + + $substituted = $this->applySubstitutions($items); + + return EntryCollection::make($substituted)->each(function ($entry) { + TypeRetrieved::dispatch($entry); + }); + } + + public function storeInCache($entry) + { + Blink::put("eloquent-entry-{$entry->id()}", $entry); + Blink::put("eloquent-entry-{$entry->uri()}", $entry); } public function save($entry) @@ -58,8 +96,7 @@ public function save($entry) $entry->model($model->fresh()); - Blink::put("eloquent-entry-{$entry->id()}", $entry); - Blink::put("eloquent-entry-{$entry->uri()}", $entry); + $this->storeInCache($entry); } public function delete($entry) diff --git a/src/Events/TypeRetrieved.php b/src/Events/TypeRetrieved.php new file mode 100644 index 00000000..cc199cf5 --- /dev/null +++ b/src/Events/TypeRetrieved.php @@ -0,0 +1,17 @@ + null, ], EntryModel::all()->mapWithKeys(fn ($e) => [$e->id => $e->data['parent'] ?? null])->all()); } + + #[Test, Group('EntryRepository#findByIds')] + public function it_gets_entries_by_ids() + { + $collection = Collection::make('pages')->routes('{slug}')->save(); + $expected = collect([ + (new Entry)->collection($collection)->slug('foo'), + (new Entry)->collection($collection)->slug('bar'), + ])->each->save(); + + $actual = (new EntryRepository(new Stache))->findByIds($expected->map->id()); + + $this->assertInstanceOf(EntryCollection::class, $actual); + $this->assertEquals($expected->map->id()->all(), $actual->map->id()->all()); + } + + #[Test, Group('EntryRepository#findByIds')] + public function it_loads_entries_from_database_given_partial_cache_when_finding_by_ids() + { + $collection = Collection::make('pages')->routes('{slug}')->save(); + $expected = collect([ + (new Entry)->collection($collection)->slug('foo'), + (new Entry)->collection($collection)->slug('bar'), + ]); + + $expected->first()->save(); + Blink::flush(); + $expected->last()->save(); + + $actual = (new EntryRepository(new Stache))->findByIds($expected->map->id()); + + $this->assertNotNull($expected->first()->id()); + $this->assertNotSame($expected->first(), $actual->first()); + $this->assertEquals($expected->first()->id(), $actual->first()->id()); + $this->assertNotNull($actual->last()); + $this->assertSame($expected->last(), $actual->last()); + } + + #[Test, Group('EntryRepository#findByIds')] + public function it_returns_entries_in_exact_order_when_finding_by_ids() + { + $collection = Collection::make('pages')->routes('{slug}')->save(); + $entries = collect([ + (new Entry)->collection($collection)->slug('foo'), + (new Entry)->collection($collection)->slug('bar'), + (new Entry)->collection($collection)->slug('baz'), + ])->each->save(); + + Blink::flush(); + + $expected = collect([2, 0, 1])->map(fn ($index) => $entries[$index]->id())->all(); + $actual = (new EntryRepository(new Stache))->findByIds($expected); + + $this->assertEquals($expected, $actual->map->id()->all()); + } + + #[Test, Group('EntryRepository#findByIds')] + public function it_skips_missing_entires_when_finding_by_ids() + { + $collection = Collection::make('pages')->routes('{slug}')->save(); + $expected = tap((new Entry)->collection($collection)->slug('foo'))->save(); + + $actual = (new EntryRepository(new Stache))->findByIds([ + $expected->id(), + 'missing', + ]); + + $this->assertEquals([$expected->id()], $actual->map->id()->all()); + } + + #[Test, Group('TypeRetrieved')] + public function it_fires_type_retrieved_event_entry_when_found() + { + Event::fake(); + $collection = Collection::make('pages')->routes('{slug}')->save(); + $expected = tap((new Entry)->collection($collection)->slug('foo'))->save(); + + (new EntryRepository(new Stache))->find($expected->id()); + + Event::assertDispatched(TypeRetrieved::class, function (TypeRetrieved $event) use ($expected) { + $this->assertSame($expected, $event->target); + + return true; + }); + } + + #[Test, Group('TypeRetrieved')] + public function it_fires_type_retrieved_event_when_not_already_in_cache() + { + Event::fake(); + $collection = Collection::make('pages')->routes('{slug}')->save(); + $expected = tap((new Entry)->collection($collection)->slug('foo'))->save(); + + Blink::flush(); + (new EntryRepository(new Stache))->find($expected->id()); + + Event::assertDispatched(TypeRetrieved::class, function (TypeRetrieved $event) use ($expected) { + $this->assertNotSame($expected, $event->target); + $this->assertEquals($expected->id(), $event->target->id()); + + return true; + }); + } + + #[Test, Group('TypeRetrieved')] + public function it_fires_type_retrieved_event_when_entry_found_by_ids() + { + Event::fake(); + $collection = Collection::make('pages')->routes('{slug}')->save(); + $expected = collect([ + (new Entry)->collection($collection)->slug('foo'), + (new Entry)->collection($collection)->slug('bar'), + (new Entry)->collection($collection)->slug('baz'), + ])->each->save(); + + (new EntryRepository(new Stache))->findByIds($expected->map->id()); + + Event::assertDispatchedTimes(TypeRetrieved::class, 3); + } + + #[Test, Group('TypeRetrieved')] + public function it_fires_type_retrieved_only_for_found_entries_when_finding_by_ids() + { + Event::fake(); + $collection = Collection::make('pages')->routes('{slug}')->save(); + $expected = tap((new Entry)->collection($collection)->slug('foo'))->save(); + + (new EntryRepository(new Stache))->findByIds([ + $expected->id(), + 'missing', + ]); + + Event::assertDispatchedTimes(TypeRetrieved::class, 1); + } + + #[Test, Group('TypeRetrieved')] + public function it_loads_from_cache_once_stored() + { + $collection = Collection::make('pages')->routes('{slug}')->save(); + $expected = (new Entry)->id(1)->collection($collection)->slug('foo'); + + $builder = Mockery::mock(\Statamic\Eloquent\Entries\EntryQueryBuilder::class); + $builder->shouldNotReceive('where'); + + $repo = (new EntryRepository(new Stache)); + $repo->storeInCache($expected); + + $this->assertSame($expected, $repo->find($expected->id())); + } }