Caching Symfony controller
При разработке этого блога пришлось изобрести очередной велосипед - Caching Symfony controller. Но сначала о том, как вообще возникла подобная задача.
Например, у меня на сайдбаре есть список категорий, список архивов и теги. Если последние получить относительно просто (одна выборка), то категории и архивы сложнее. Для получения категорий нужно выбрать деревья (категории могут быть вложенными) и подзапросами получить количество заметок в категориях (довольно монстроузно получилось, посмотреть можно здесь). Для архивов нужно пройтись по всем заметкам и составить список годов/месяцев. Действия не слишком сложные, но все равно можно избавится от их выполнения на каждом запросе.
Общая схема оптимизации следующая: получаем необходимые данные с базы, обрабатываем, кешируем их. На следующих реквестах уже будем отдавать закешированные данные. Если что-то поменяется (в моем случае добавится/обновится заметка), чистим кеш, и на следующем реквесте данные опять обновятся и закешируются.
Можно было бы в каждом сервисе, предоставляющем данные, держать свой кеш. Но хотелось придумать какое-то общее решение. И вот что с этого получилось.
Я вывожу компоненты сайдбара с помощью сабрендера контроллера. Оптимальнее решение для компонента, который должен выводится на всех страницах сложно придумать. Остальные варианты менее привлекательные:
- У каждом екшене передавать в темплейт необходимы переменные (бред)
- Инжектить переменные лисенером (слишком неочевидно)
Поэтому была создана 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, к примеру.