Remember this - Caching in Kirby

How to make blueprints with query in multiselect option with fetch faster?

The problem with fetch

Lets imagine articles where we want to reference related articles by their autoid or boostid.

site/blueprints/pages/article.yml

# other fields ...
category:
  label: Related articles
  type: multiselect
  options: query
  query:
    fetch: site.find('articles').childrenAndDrafts
    text: "{{ page.title }}"    # boosted
    value: "{{ page.boostid }}" # boosted

In your frontend you do something like this

$recentArticles = page('articles')->filterBy('date', '>=', strtotime('-1 week'));
foreach($recentArticles as $article) {
   echo Html::a($article->url(), $article->title());
}

Using multiselect with queries to fetch will cause kirby to load these for every object you create with that blueprint. Not just in the panel but also in your own frontend even if you never need that data. But sadly that is nothing kirby can fix easily. So in the example above it will be okay-ish since kirby keeps loaded pages in memory.

But what if kirby does not cache the data for us?

site/blueprints/pages/article.yml

# other fields ...
category:
  label: Related twitter Posts from API
  type: multiselect
  options: query
  query:
    fetch: kirby.collection('twitterPosts') # needs a cache
    text: "{{ arrayItem.title }}"
    value: "{{ arrayItem.url }}"

We can even make this worse when we have a usecase where we wrap it in a structure. Then it will be loaded for every single row again and again.

site/blueprints/pages/article.yml

myFavPostsWithCustomRating:
  type: structure
  fields:
    rating:
      type: number
      min: 0
      max: 5
    twitter_url:
      label: Titter Posts from API
      type: multiselect
      options: query
      query:
        fetch: kirby.collection('twitterPosts') # needs a cache
        text: "{{ arrayItem.title }}"
        value: "{{ arrayItem.url }}"
$recentArticles = page('articles')->filterBy('date', '>=', strtotime('-1 week'));
foreach($recentArticles as $article) {
   foreach($article->myFavPostsWithCustomRating()->toStructure() as $post) {
      echo Html::a($post->twitter_url(), str_repeat('⭐', $post->rating()->toInt()));
   }
}

That might be a lot of calls to kirby.collection('twitterPosts').

Solution with a static cache

You could add the cache pretty much anywhere you like. A model, sitemethods, pagemethods… for the usecase above i would suggest using a collection from an file definition. While you can use the static keyword in functions there are some edgecases to consider and i prefer using a wrapper class.

site/collections/twitterPosts.php

<?php

class TwitterPosts
{
    static $cache = null;
    static function loadWithCache(): ?array
    {
        // if cached then return that
        if(static::$cache) return static::$cache;

        static::$cache = myLogicToLoadThePostsAndTransformThemToAnArray();
        return static::$cache;
    }
}

return function () {
    return TwitterPosts::loadWithCache();
};

But what if you have have a pages collection that takes a while to build and you do not want to do that again and again as well? Simplified anything with find, index, filter, sort, group might be a good place to add a cache.

 # needs a cache
fetch: site.index(true).filterBy('intendedTemplate', 'in', ['person', 'organisation', 'document', 'place']
fetch: kirby.collection('pagesThatCanBeRefrenced')

site/collections/pagesThatCanBeRefrenced.php

<?php

class PagesThatCanBeReferenced
{
    static $cache = null;
    static function load(): ?\Kirby\Cms\Pages
    {
        // if cached then return that
        if(static::$cache) return static::$cache;

        $collection = site()->index(true)->filterBy('intendedTemplate', 'in', [
            'person',
            'organisation',
            'document',
            'place'
        ]);

        static::$cache = $collection;
        return static::$cache;
    }
}

return function () {
    return PagesThatCanBeReferenced::load();
};

If you have a really big index (like 10k or mor pages) you might want to avoid calling index on every request. You can do that like this but its a bit advanced…

<?php

class PagesThatCanBeReferencedWithoutIndex
{
    static $cache = null;
    static function load(): ?\Kirby\Cms\Pages
    {
        // if cached then return that
        if(static::$cache) return static::$cache;

        // use lapse to cache the diruri
        // this will avoid index()
        $cachedDirUris = \Bnomei\Lapse::io(
            static::class, // a key for the cache
            function () {
                $collection = site()->index(true)->filterBy('intendedTemplate', 'in', [
                    'person',
                    'organisation',
                    'document',
                    'place'
                ]);
                return array_values($collection->map(function($page) {
                    return $page->diruri();
                }));
            },
            10 // expire in 10 minutes
        );
        
        // use bolt from autoid/boost to get pages quickly
        $pages = array_map(function($diruri) {
            return bolt($diruri);
        }, $cachedDirUris);
        // remove those that bolt did not find
        $pages = array_filter($pages, function($page) {
            return is_null($page) ? false : true;
        });

        $collectionFromDirUris = new \Kirby\Cms\Pages($pages);

        static::$cache = $collectionFromDirUris;
        return static::$cache;
    }
}

return function () {
    return PagesThatCanBeReferencedWithoutIndex::load();
};

Hope you learned something new. Happy caching!

5 Likes