XF 2.2 Caching results of a query

Orit

Active member
Hi
I have a couple of custom widgets that execute database queries using finders. These queries don’t need to run frequently because the results do not change often.
Our forum board is fairly busy, sometimes with 6,000 simultaneous users, and executing these queries for every user is overloading our database and affecting performance.

How can I cache the results of these finder queries for at least 30 minutes, and then refresh the cache only when it expires?

Thanks!
 
If a cache is configured, you can get the Symfony Cache Adapter via:
PHP:
/** @var \Symfony\Component\Cache\Adapter\AbstractAdapter|null $cache */
$cache = \XF::app()->cache('', true, false);

You can see the documentation for further details, but the gist of it is:
PHP:
/** @var \Symfony\Component\Cache\Adapter\AbstractAdapter|null $cache */
$cache = \XF::app()->cache('', true, false);
if ($cache)
{
    // get an item if it exists, or compute it otherwise
    $value = $cache->get(
        'someKey',
        function (\Symfony\Contracts\Cache\ItemInterface $item): string
        {
            $item->expiresAfter(3600); // TTL

            return 'someValue';
        }
    );

    // delete an item
    $cache->delete('someKey');
}

You can cache the result of an expensive query, but you can't cache entity objects themselves since they can't be serialized. If you can cache the primary keys, look ups should be pretty fast. Otherwise you may wish to consider caching the rendered template string.
 
PHP:
Code:
/** @var \Symfony\Component\Cache\Adapter\AbstractAdapter|null $cache */
$cache = \XF::app()->cache('', true, false);
Thank you @Jeremy P
Is this for XF 2.2?
I can't seem to find the files. Do I need to install the adapter?

you can't cache entity objects themselves since they can't be serialized.
This, indeed, was what I was planning to do...
How can I cache the rendered template string?
 
How can I cache the rendered template string?
I managed to get it.
in the beginning of the render() function:
PHP:
$cache = \XF::app()->cache();
$cacheKey = 'widget_latest_thread_posts_' . $threadId . '_' . md5(json_encode($options));

// Try fetching the cached template first
$cachedOutput = $cache->fetch($cacheKey);
if ($cachedOutput) {
    return $cachedOutput;
}

and before the closing tag:
PHP:
// Render the template to a string
$templater = \XF::app()->templater();
$renderedTemplate = $templater->renderTemplate('public:widget_latest_thread_posts', $viewParams);

// Cache the rendered output
$cache->save($cacheKey, $renderedTemplate, 3600); // Cache for 1 hour
\XF::logError('Cached template under key: ' . $cacheKey); // for debugging

return $renderedTemplate;

Apparently, <xf:css> and <xf:js> tags do not get rendered in the cached template.
Changing <xf:css> to a simple <style> tag solves this. what would you suggest for <xf:css src="myCss.less/ >?
and is there a suggestion for inserting JavaScript, apart from having them in a separate widget?
Thank you!!
 
Is this for XF 2.2?
Sorry, I didn't see the prefix. XF 2.2 used Doctrine cache, though XF 2.3 does provide a backwards-compatible API.

Apparently, <xf:css> and <xf:js> tags do not get rendered in the cached template.
Changing <xf:css> to a simple <style> tag solves this. what would you suggest for <xf:css src="myCss.less/ >?
and is there a suggestion for inserting JavaScript, apart from having them in a separate widget?
Thank you!!
My suggestion would be to use two templates, one which is just the markup generated using your queries and the other which you pass the rendered string into alongside your JS/CSS/whatever else:
PHP:
/** @var \Doctrine\Common\Cache\CacheProvider|null $cache */
$cache = \XF::app()->cache();
$html = $cache ? $cache->fetch('cacheKey') : false;
if (!$html)
{
    // ...your code here...
    $templater = \XF::app()->templater();
    $innerViewParams = [
        'data' => $data,
    ];
    $html = $templater->renderTemplate(
        'some_inner_template',
        $innerViewParams
    );

    if ($cache)
    {
        $cache->save('cacheKey', $html, 3600);
    }
}

$viewParams = [
    'html' => $html,
];
return $this->renderer('some_outer_template', $viewParams);

HTML:
<xf:css src="some_template.less" />
<xf:js src="some/js.js" />

{{ $html|raw }}
 
/** @var \Doctrine\Common\Cache\CacheProvider|null $cache */ $cache = \XF::app()->cache(); $html = $cache ? $cache->fetch('cacheKey') : false; if (!$html) { // ...your code here... $templater = \XF::app()->templater(); $innerViewParams = [ 'data' => $data, ]; $html = $templater->renderTemplate( 'some_inner_template', $innerViewParams ); if ($cache) { $cache->save('cacheKey', $html, 3600); } } $viewParams = [ 'html' => $html, ]; return $this->renderer('some_outer_template', $viewParams);
Thank you @Jeremy P
That really helped!
(I got the caching going but had some trouble with a slider I was using. )

The problem I can't find a solution to is that the $widget variable does not get passed on to the 'some_inner_template' template (as it is passed on in every other widget).
Am I missing something, or is it just not possible?

Thanks!
 
Am I missing something, or is it just not possible?
It's possible, but it's not handled automatically. If you look at \XF\Widget\AbstractWidget::renderer, you'll see it just merges in a few template params via \XF\Widget\AbstractWidget::getDefaultTemplateParams. You can accomplish the same thing in the inner template by merging them before rendering the template:

PHP:
$viewParams = array_replace($this->getDefaultTemplateParams('render'), $viewParams);
 
Last edited:
It's possible, but it's not handled automatically. If you look at \XF\Widget\AbstractWidget::render, you'll see it just merges in a few template params via \XF\Widget\AbstractWidget::getDefaultTemplateParams. You can accomplish the same thing in the inner template by merging them before rendering the template:

PHP:
$viewParams = array_replace($this->getDefaultTemplateParams('render'), $viewParams);
Thank you so much!
That was ever so simple :D
 
Back
Top Bottom