Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions src/Entries/EntryRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions src/Events/TypeRetrieved.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Statamic\Eloquent\Events;

use Statamic\Events\Event;

class TypeRetrieved extends Event
{
/**
* Fired when a type if requested through a Repository and found, either in
* the cache or loaded from the database. Currently, only the implemented
* by the EntryRepository.
*
* @param \Statamic\Contracts\Entry\Entry $target
*/
public function __construct(public $target) {}
}
154 changes: 154 additions & 0 deletions tests/Entries/EntryRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Mockery;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Eloquent\Entries\Entry;
use Statamic\Eloquent\Entries\EntryModel;
use Statamic\Eloquent\Entries\EntryRepository;
use Statamic\Eloquent\Events\TypeRetrieved;
use Statamic\Entries\EntryCollection;
use Statamic\Events\CollectionTreeSaved;
use Statamic\Facades\Blink;
use Statamic\Facades\Collection;
use Statamic\Stache\Stache;
use Tests\TestCase;
Expand Down Expand Up @@ -234,4 +239,153 @@ public function it_updates_the_parents_of_specific_entries_in_a_collection()
4 => 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()));
}
}