XF 2.3 Best practice for caching finder results (Entity Collections) in a controller?

Atakan KOC

Active member
Licensed customer
Hi everyone,

I'm building a custom addon for XenForo 2.3.x and I'm trying to implement caching in my controller to reduce database queries on my homepage. I'd appreciate some guidance on the correct approach.

What I'm trying to do:
Cache the result of multiple finder queries (threads, statistics) so they don't hit the database on every page load.

My current approach:

Fetching from cache:
PHP:
$cache = $this->app()->cache();
$key = 'index_data_v1';
$cachedData = $cache ? $cache->fetch($key) : null;

if ($cachedData !== false && $cachedData !== null) {
    return $this->view('MyAddon:Page\Index', 'my_template', $cachedData);
}

Saving to cache:
PHP:
$viewParams = [
    'news'          => $news,           // XF Entity Collection
    'announcements' => $announcements,  // XF Entity Collection
    'latestThreads' => $latestThreads,  // XF Entity Collection
    'statistics'    => [
        'threads'  => $forumStats['threads']  ?? 0,
        'messages' => $forumStats['messages'] ?? 0,
        'users'    => $forumStats['users']    ?? 0,
    ]
];

if ($cache) {
    $cache->save($key, $viewParams, 300);
}

My questions:

  1. Can XF Entity Collections (returned by ->fetch()) be serialized and stored in cache reliably? Or does $cache->save() silently fail or store corrupted data when passed objects?
  2. Is $this->app()->cache() the correct way to access the cache layer in a controller, or should I be using SimpleCache instead?
  3. What is the recommended pattern for caching finder results in XF 2.3? Should I cache only the entity IDs and re-fetch on cache hit, or convert entities to plain arrays?
  4. Does $cache->fetch() return false or null on a cache miss? I want to make sure my check is correct.

Any help or pointers to the relevant source files would be greatly appreciated. Thanks!
 
I recently ran into an issue where caching raw Entity Collections (from finder results) wasn't working as expected. Since Entities are complex objects with various relations, they don't always serialize/unserialize reliably in the cache layer.

The fix was to "dehydrate" the entities into plain PHP arrays before saving them. This made the cache lightweight and much more stable. Here is the approach I used:

PHP:
public function actionIndex()
{
    $cache = $this->app()->cache();
    $key = 'my_index_data';

    if ($cache && ($viewParams = $cache->fetch($key)))
    {
        return $this->view('MyAddon:Page\Index', 'my_template', $viewParams);
    }

    $newsThreads = $this->finder('XF:Thread')->with('FirstPost')->limit(10)->fetch();

    $viewParams = [
        'newsThreads' => $this->ThreadsCache($newsThreads),
        'stats' => $this->app()->forumStatistics,
    ];

    if ($cache) $cache->save($key, $viewParams, 300);

    return $this->view('MyAddon:Page\Index', 'my_template', $viewParams);
}

protected function ThreadsCache($entities)
{
    $output = [];

    foreach ($entities AS $entity)
    {
        $thread = ($entity instanceof \XF\Entity\Post) ? $entity->Thread : $entity;
        $firstPost = ($entity instanceof \XF\Entity\Post) ? $entity : $entity->FirstPost;

        $output[] = [
            'thread_id' => $thread->thread_id,
            'user_id'=> $thread->user_id,
            'username' => $thread->username,
            'title' => $thread->title,
            'link' => \XF::app()->router('public')->buildLink('threads', $thread),
            'avatar_url' => $thread->User ? $thread->User->getAvatarUrl('s', null, true) : null,
            'snippet' => $firstPost ? \XF::app()->stringFormatter()->snippetString($firstPost->message, 300) : '',
            'User' => $thread->User
        ];
    }
  
    return $output;
}

Hope this helps anyone struggling with XF 2.3 caching!

Note: Please keep in mind that the code below is not a complete, standalone addon. It is a proof-of-concept example to demonstrate how to handle caching logic in your own controllers. You should adapt the methods and field names to fit your specific addon structure.
 
You do not want to save the entire entity when caching.

The common practice is to cache the list of ids and then do a fetch on those ids and allow XF to filter results based on permissions. This ensures that even if the content is moved or removed you do not show out of date content.

If you do summarize the results into a plain-data-object like that, you should store the node_id and do permission checks manually to determine if the guest user can see contents of that node node.

The primary id + database lookup is recommended for correctness, and yes it requires a database lookup but database lookups via primary ids are very fast.
 
  • Like
Reactions: Bob
Back
Top Bottom