ru en

Caching Symfony controller

При разработке этого блога пришлось изобрести очередной велосипед - Caching Symfony controller. Но сначала о том, как вообще возникла подобная задача.

Например, у меня на сайдбаре есть список категорий, список архивов и теги. Если последние получить относительно просто (одна выборка), то категории и архивы сложнее. Для получения категорий нужно выбрать деревья (категории могут быть вложенными) и подзапросами получить количество заметок в категориях (довольно монстроузно получилось, посмотреть можно здесь). Для архивов нужно пройтись по всем заметкам и составить список годов/месяцев. Действия не слишком сложные, но все равно можно избавится от их выполнения на каждом запросе.

Общая схема оптимизации следующая: получаем необходимые данные с базы, обрабатываем, кешируем их. На следующих реквестах уже будем отдавать закешированные данные. Если что-то поменяется (в моем случае добавится/обновится заметка), чистим кеш, и на следующем реквесте данные опять обновятся и закешируются.

Можно было бы в каждом сервисе, предоставляющем данные, держать свой кеш. Но хотелось придумать какое-то общее решение. И вот что с этого получилось.

Я вывожу компоненты сайдбара с помощью сабрендера контроллера. Оптимальнее решение для компонента, который должен выводится на всех страницах сложно придумать. Остальные варианты менее привлекательные:

  1. У каждом екшене передавать в темплейт необходимы переменные (бред)
  2. Инжектить переменные лисенером (слишком неочевидно)

Поэтому была создана twig функция, которая работает почти так же, как и стандартная

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

но которая при этом могла бы кешировать результат рендера:

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

Для этого создадим twig extension, в который сразу заинжектим 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;
    }
}

Конструктор получает кешер (можно использовать любой, в примере APC) и время, на которое нужно кешировать (0 для неограниченого времени жизни).

Реализация renderCached, скорее всего, не слишком удачная. Ключ для кеша генерится с использованием имени контроллера, опций и параметров запроса. (Смысл этого, надеюсь понятен). Целесообразность кеширования подобных контроллеров остается под вопросом (сам не использовал, я кешировал контроллеры без сложных параметров и уж точно без параметров запроса).

Конфигурация сервиса кеша:

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

Конфигурация twig extension сервиса:

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

В принципе, на этом все. Получаем функцию render_cached, которая работает так же, как и оригинальная render, но при этом по пути кеширует результат рендеринга на сконфигурированное время. Если приложение в каком-то месте повлияло на содержимое, нужно очистить кеш.

Осталось только напомнить о том, что использовать подобное решение имеет смысл только в простейших случаях. В более сложных, гуглите в сторону Varnish, к примеру.