Skip to content

Conversation

@nadinengland
Copy link
Owner

@nadinengland nadinengland commented Apr 5, 2025

This PR adds a new TypeRetrieved event, intended for repositories to emit when types (e.g. entries) are loaded by identifier.

<?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) {}
}

It builds off another open PR at the moment.

(new Entry)->id(1)->slug('home');
(new Entry)->id(2)->slug('about');

\Statamic\Facades\Entry::find(1); // Fires
\Statamic\Facades\Entry::find(10); // Doesn't fire
\Statamic\Facades\Entry::findByUri('/about'); // Fires

Notably, this is does not fire via the methods that are indirectly loading entires. I.e. ->whereCollection() as the entries are not being found by identifier.

This is part of a wider piece of work I've start to add a fairly large reduction in DB queries to the statamic/eloquent-driver. The principal is to route as many modules of the system as possible through the repositories so that they may emitting this event.

In my own projects, I rebind all the repositories via the container to do this, but it would be useful if the repositories did it themselves - hence the PR.

Utility and performance gain

Once in emitting, you can do an interesting optimisation of statamic.web routes. Rather than loading each entry one by one whilst Statamic Values are being augmented, instead, at end of each request you can store into the cache a list of IDs what were requested. Then, when next rendering the page, eager load them into the blink cache.

To be clear, this is not simply caching the entries, but rather just their IDs. We gain the luxury of not having to think about cache invalidation.

The rationale is that its typically cheaper to do one DB query for 20 entries than it is to do 20 DB queries.

There are some edge cases:

  • Two many entries loaded. Following a publishing of an entry by a content editor, in between frontend requests, one of the entries are loaded from the database but isn't use.
  • Not enough entries loaded. Again, the content editor makes some changes in between requests, this time removing a link to an entry. The repository will not find the entry in the blink cache and fallback to a DB query to retrieve it.

In both cases the type usage cache isn't up to date, but only for one request. As soon as the page renders again, the exact type usage will be save. In both cases there is still a very high likelyhood that the number of DB queries will be dramatically reduced.

Pseudo code for simplicity below:

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app->singleton(TypeUsage::class);
        Route::prependMiddlewareToGroup('statamic.web', RecordTypeUsage::class);
        Event::listen(TypeRetrieved::class, TypeUsage::class);
    }
}

class RecordTypeUsage
{
    public function __construct(public TypeUsage $usage)
    {
    }

    public function handle(Request $request, Closure $next)
    {
        $this->usage->load($request->path());
        $response = $next($request);
        $this->usage->save($request->path());

        return $response;
    }
}

class TypeUsage
{
    protected array $cached = [];
    protected array $actual = [];

    public function load(string $path)
    {
        $usage = Cache::get("type-usage::{$path}") ?? [];

        $items = Entry::query()->whereIn('id', $usage ?? [])->get();
        $items->each(fn ($entry) => $this->entries->storeInCache($entry));
    }

    public function save(string $path)
    {
        $actual = collect($this->actual)->keys()->sort()->all();
        $cached = collect($this->cached)->keys()->sort()->all();

        // No DB write needed if the current usage is the same as last time.
        if ($actual === $cached) {
            return;
        }

        Cache::put("type-usage::{$path}");
    }
}

Note: the example above is only for entries, but could be expanded to the other types in Statamic, e.g. assets, terms etc.

Intention to implement further

Should this is something that could be merged, I'd be happy to extend this functionality to all the repositories that use find and findBy.

@nadinengland nadinengland force-pushed the feature/entries-find-by-ids branch from d3689d9 to 9f6271c Compare April 5, 2025 22:19
@nadinengland nadinengland force-pushed the feature/type-retrieval branch from 9ac2d77 to dcde903 Compare April 5, 2025 22:24
@nadinengland
Copy link
Owner Author

Note: might be able to mention this natively with the existing hooks that got added last year: statamic/cms#9625

@nadinengland nadinengland force-pushed the feature/entries-find-by-ids branch from 9f6271c to 501f528 Compare October 6, 2025 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants