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!