Caching Symfony controller

During developing this blog I invented one more bicycle for Caching Symfony controller. But first of all lets see how did this task arose.

For example, I have a list of categories, archives and tags on a sidebar. It is relatively easy to get last one (one query), but much harder to get list of categories and archives. For getting categories, we need to select trees (categories can be nested) and using subqueries inside queries get number of articles in every category (result is a little bit monster). For getting the archives list, we need iterate over all articles and gather list of years/months. All this actions isn't very sophisticated, but it is better to avoid them.Optimisation idea is following: we select data, handle it and put into cache. On next requests we will get data from cache. If something will be changed (in my case article will be added or edited), we will clear cache and it will be repopulated on next request.

This is basic idea. Obvious, we can store and handle cache in every data service. But it is not very convenient. And I tried to implement some common solution.

I render sidebar parts using a Controller subrender. It is difficult to guess the optimal solution for a component which has to be shown on all pages. Other less attractive variants: 

1. Pass variables to templates in every action.

2. Inject variables using a listener (Not obvious and complicate understanding)

That's why was created a Twig function, which works most the same as standard:

{{ render(controller('HarentiusBlogBundle:Sidebar:categories', { showNumber: false })) }}

but can cache result of the rendering:

{{ render_cached(controller('HarentiusBlogBundle:Sidebar:categories', { showNumber: false })) }}

We will create twig extension, and inject apc cache:

<?php

namespace Harentius\BlogBundle\Twig;

use Doctrine\Common\Cache\CacheProvider;
use Symfony\Bridge\Twig\Extension\HttpKernelExtension;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;

class BlogExtension extends HttpKernelExtension
{
    /**
     * @var CacheProvider
     */
    private $cache;

    /**
     * @var int
     */
    private $cacheLifeTime;

    /**
     * @var SettingsProvider
     */
    private $settingsProvider;

    /**
     * @param FragmentHandler $handler A FragmentHandler instance
     * @param CacheProvider $cache
     * @param int $cacheLifeTime
     */
    public function __construct(
        FragmentHandler $handler,
        CacheProvider $cache,
        $cacheLifeTime
    ) {
        parent::__construct($handler);
        $this->cache = $cache;
        $this->cacheLifeTime = $cacheLifeTime;
    }

    /**
     * @return array
     */
    public function getFunctions()
    {
        return array(
            new \Twig_SimpleFunction('render_cached', [$this, 'renderCached'], [
                'is_safe' => ['html']
            ]),
        );
    }

    /**
     * @param $controllerReference
     * @param array $options
     * @return string
     */
    public function renderCached(ControllerReference $controllerReference, $options = [])
    {
        $key = $controllerReference->controller;

        if ($controllerReference->attributes !== []) {
            $key .= json_encode($controllerReference->attributes);
        }

        if ($controllerReference->query !== []) {
            $key .= json_encode($controllerReference->query);
        }

        if ($this->cache->contains($key)) {
            return $this->cache->fetch($key);
        }

        $renderedContent = $this->renderFragment($controllerReference, $options);
        $this->cache->save($key, $renderedContent);

        return $renderedContent;
    }
}

Constructor gets cache instance (can be used anyone, in this example APC) and time to live cache (0 unlimited TTL).

renderCached method implementation seems to be not very optimal. Cache key generates using the Controllers name, options and query parameters. (Hope, purpose of this is obvious). Expediency of caching such controllers is remains in question (I hadn't used that, I just cached controllers without complicated and query string).

Cache service configuration:

harentius_blog.sidebar.cache:
    class: Doctrine\Common\Cache\ApcCache
    calls:
        - [ setNamespace, [ %harentius_blog.cache.apc_global_prefix%_sidebar ] ]

Twig extension service configuration:

harentius_blog.twig.blog_extension:
    class: Harentius\BlogBundle\Twig\BlogExtension
    arguments:
        - @fragment.handler
        - @harentius_blog.sidebar.cache
        - %sidebar_cache_lifetime%
    tags:
        - { name: twig.extension }

That's all. We implemented render_cached function, which works mostly the same as original render, but caching result of rendering tor TTL. If application influenced on content, cache clear required.

I'd like to stress, that using this solution has sense only in the simplest cases. For more sophisticated use Varnish, for example.