Skip to content
Caution: You are browsing the legacy symfony 1.x part of this website.

День 21: Кэш

Symfony version
Language
ORM

Сегодня мы поговорим о кэшировании. У Symfony есть много встроенных стратегий кэширования. Например, конфигурационные YAML-файлы сначала конвертируются в PHP и затем кэшируются в файловой системе. Мы так же видели, что модули, созданные с помощью генератора админки, кэшируются для улучшения производительности.

Но сегодня мы будем говорить о другом кэше: кэш HTML. Для улучшения производительности Вашего веб-сайта, вы можете кэшировать целые страницы HTML или только их части.

Создание нового окружения

По умолчанию функциональность кэширования шаблонов Symfony включена в конфигурационном файле settings.yml для окружения prod, но не для test и dev:

prod:
  .settings:
    cache: true
 
dev:
  .settings:
    cache: false
 
test:
  .settings:
    cache: false

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

Для того, чтобы попробовать систему кэширования в Jobeet, мы создадим окружение cache, подобное окружению prod, но с журналированием и информацией для отладки, доступной в окружении dev.

Создайте контроллер, связанный с новым окружением cache скопировав dev контроллер web/frontend_dev.php в web/frontend_cache.php:

// web/frontend_cache.php
if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1')))
{
  die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}
 
require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');
 
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'cache', true);
sfContext::createInstance($configuration)->dispatch();

Это все, что нужно сделать. Теперь мы можем использовать новое окружение cache. Единственное отличие - это второй аргумент для метода getApplicationConfiguration(), где указывается имя окружения - cache.

Вы можете протестировать окружение cache в вашем браузере с помощью вызова его контроллера:

http://www.jobeet.com.localhost/frontend_cache.php/

note

Скрипт контроллера начинается с кода, который проверяет, что он вызван с локального IP-адреса. Это меры безопасности для защиты от вызова контроллера на промышленных серверах. Мы поговорим об этом более детально в завтрашнем уроке.

Сейчас окружение cache наследует конфигурацию по умолчаюнию. Измените конфигурационный файл settings.yml для добавления специфичной конфигурации для окружения cache:

# apps/frontend/config/settings.yml
cache:
  .settings:
    error_reporting: <?php echo (E_ALL | E_STRICT)."\n" ?>
    web_debug:       true
    cache:           true
    etag:            false

В этих настройках, была включена функциональность кэша шаблонов с помощью настройки cache и веб-панель отладки - с помощью настройки web_debug.

Поскольку конфигурация по умолчанию сохраняет все настройки в кэше, Вам нужно очистить его перед тем, как Вы увидите изменения в Вашем браузере:

$ php symfony cc

Теперь, если Вы обновите страницу в Вашем браузере, должна появиться веб-панель отладки в верхнем-правом углу страницы, как это было для окружения dev.

Конфигурация кэша

Кэш шаблонов Symfony может быть настроен в конфигурационном файле cache.yml. Конфигурацию по умолчанию для приложения можно найти в apps/frontend/config/cache.yml:

default:
  enabled:     false
  with_layout: false
  lifetime:    86400

По умолчанию, поскольку все страницы могут содержать динамическую информацию, кэш глобально отключен (enabled: false). Нам не нужно менять эту настройку, поскольку мы будем включать кэш на постраничной базе.

Настройка lifetime определяет время жизни кэша в секундах
(86400 соответствует одним суткам) на стороне сервера.

tip

Вы можете это сделать другим способом: включить кэш глобально и затем отключить его на специальных страницах, которые не могут быть кэшированы. Это зависит от того где меньше работы для Вашего приложения.

Кэш страницы

Поскольку домашняя страница Jobeet возможно будет наиболее посещаемой страницей веб-сайта, вместо запроса данных из базы данных каждый раз, когда пользователь приходит на нее, она будет кэширована.

Создайте файл cache.yml для модуля sfJobeetJob:

# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml
index:
  enabled:     true
  with_layout: true

tip

Конфигурационный файл cache.yml имеет те же свойства что и любой другой файл конфигурации как например view.yml. Это означает что вы например можете включить кэш для всех действий модуля используя специальный ключ all.

Если Вы обновите страницу в браузере, Вы увидите что Symfony украсил страницу рамкой показывая что содержимое было кэшировано:

Fresh Cache

Рамка дает некоторую ценную информацию о кэше для отладки, как например время жизни кэша и его возраст.

Если Вы обновите страницу еще раз, цвет рами будет изменен с зеленого на желтый, указывая на то что страница была получена из кэша:

Cache

Также заметьте, что не было сделано запросов к базе данных во втором случае, как показано на отладочной веб-панели.

tip

Даже если язык будет изменен пользователем, кэш все еще работает, так как язык включен в URL.

Когда страница может быть кэширована, и если кэш еще не существует, Symfony сохраняет объект ответа в кэше в конце запроса. Для всех будущих запросов, Symfony будет отсылать кэшированый ответ без вызова контроллера:

Page Cache Flow

Это сильно влияет на производительность и Вы можете самостоятельно измерить это, используя такие инструменты, как например JMeter.

note

Входящий запрос с параметрами GET или отосланный с помощью методов POST, PUT, или DELETE не кэшируется Symfony, независимо от конфигурации.

Страница создания работы также может быть кэширована:

# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml
new:
  enabled:     true
 
index:
  enabled:     true
 
all:
  with_layout: true

Поскольку две страницы могут быть кэшированы с шаблоном, мы создали секцию all, которая определяет конфигурацию по умолчанию для всех действий модуля sfJobeetJob.

Очистка кэша

Если вы хотите очистить кэш страницы, вы можете использовать задачу cache:clear:

$ php symfony cc

Задача cache:clear очищает все кэши Symfony, сохраненные в главной папке cache/. Она также принимает опции для выборочной очистки некоторых частей кэша. Для очистки только кэша шаблонов для окружения cache, используйте опции --type и --env:

$ php symfony cc --type=template --env=cache

Вместо очистки кэша каждый раз, когда вы делаете изменения, вы можете так же отключить кэш с помощью добавления любой строки запроса в URL, или используя кнопку "Игнорировать кэш" из отладочной веб-панели:

Web Debug Toolbar

Кэширование действия

Иногда Вы не можете сохранить всю страницу в кэш, но шаблон действия сам по себе может быть кэширован. Попробуйте другой вариант, Вы можете кэшировать все, кроме шаблона.

Для приложения Jobeet, мы не можем кэшировать всю страницу из-за фрагмента "история вакансий".

Измените конфигурацию кэша для модуля job следующим образом:

# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml
new:
  enabled:     true
 
index:
  enabled:     true
 
all:
  with_layout: false

Изменив настройку with_layout на false, Вы отключили кэширование шаблона.

Очистите кэш:

$ php symfony cc

Обновите страницу в Вашем браузере, чтобы увидеть отличия:

Action Cache

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

Action Cache Flow

Кэширование фрагментов и компонентов

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

Partial Cache

Давайте кэшируем компонент language, создав файл cache.yml для модуля sfJobeetLanguage:

# plugins/sfJobeetPlugin/modules/sfJobeetLanguage/config/cache.yml
_language:
  enabled: true

Конфигурирование кэша для фрагмента или компонента достигается простым добавлением записи с его именем в файл конфигурации. Опция with_layout не берется в расчет для этого типа кэша, поскольку в этом нет смысла:

Partial and Component Cache Flow

sidebar

Контекстное кэширование - когда использовать?

Тот же самый компонент или фрагмент может быть использован во многих разных шаблонах Фрагмент вакансий _list.php, например, используется модулями sfJobeetJob и sfJobeetCategory. Поскольку вывод будет всегда тот же самый, фрагмент не зависит от контекста, в котором он используется, и кэш тот же самый для всех шаблонов (кэш по-прежнему очевидно отличается для разных наборов параметров).

Но иногда, вывод фрагмента или компонента отличается в зависимости от действия, в которое он включен (подумайте о панели блога например, которая немного отличается для домашней страницы и для страницы с постом в блоге). В этих случаях фрагмент или компонент контекстный, и кэш должен быть настроен соответственно с помощью установки опции contextual в true:

_sidebar:
  enabled:    true
  contextual: true

Формы в кэше

Сохранение страницы создания вакансии в кэш проблематично, поскольку она содержит форму. Для лучшего понимания проблемы, сходите на страницу "Post a Job" в Вашем браузере для создания кэша. Затем очистите куки сессии, и попробуйте сохранить вакансию. Вы должны увидеть сообщение об ошибке - "CSRF атака":

CSRF and Cache

Почему? Поскольку мы настроили секретный CSRF, когда создавали приложение frontend, Symfony вкладывает ключ CSRF во все формы. Для защиты Вашего приложения от CSRF атак, этот ключ уникален для данного пользователя и для данной формы.

Первый раз, когда страница отображается, сгенерированная HTML-форма сохраняется в кэш с ключом текущего пользователя. Когда следом заходит другой пользователь, будет отображена страница из кэша с CSRF-ключом первого пользователя. Когда форма будет отослана, ключ не совпадет и появится ошибка.

Как мы можем это исправить, поскольку кэширование формы выглядит разумным? Форма создания вакансии не зависит от пользователя, и она ничего не меняет для текущего пользователя. В этом случае CSRF-защита не нужна, и мы можем совсем удалить ключ CSRF:

// plugins/sfJobeetPlugin/lib/form/doctrine/PluginJobeetJobForm.class.php
abstract PluginJobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    $this->disableLocalCSRFProtection();
  }
}

После этого изменения, очистите кэш и попробуйте тот же самый сценарий, чтобы увидеть, что он теперь работает, как ожидалось.

Та же конфигурация должна быть применена для формы смены языка сайта, которая содержится в шаблоне и будет сохранена в кэше. Поскольку использована sfLanguageForm по умолчанию, вместо создания нового класса можно просто удалить CSRF-ключ, давайте сделаем это для действия и компонента модуля sfJobeetLanguage:

// plugins/sfJobeetPlugin/modules/sfJobeetLanguage/actions/components.class.php
class sfJobeetLanguageComponents extends sfComponents
{
  public function executeLanguage(sfWebRequest $request)
  {
    $this->form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr')));
    $this->form->disableLocalCSRFProtection();
  }
}
 
// plugins/sfJobeetPlugin/modules/sfJobeetLanguage/actions/actions.class.php
class sfJobeetLanguageActions extends sfActions
{
  public function executeChangeLanguage(sfWebRequest $request)
  {
    $form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr')));
    $form->disableLocalCSRFProtection();
 
    // ...
  }
}

The disableLocalCSRFProtection() method disables the CSRF token for this form.

Удаление кэша

Каждый раз, когда пользователь добавляет и активирует вакансию, должен быть обновлен список новых вакансий на домашней странице.

Поскольку нам не нужно, чтобы вакансии появлялись на домашней странице в реальном времени, лучшая стратегия - это уменьшить время жизни кэша:

# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml
index:
  enabled:  true
  lifetime: 600

В конфигурации по умолчанию вместо одного дня кэш для домашней страницы будет автоматически удаляться каждые десять минут.

Но если Вы хотите обновлять домашнюю страницу сразу после того, как пользователь активирует новую вакансию, измените метод executePublish() модуля sfJobeetJob и добавьте ручную очистку кэша:

// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php
public function executePublish(sfWebRequest $request)
{
  $request->checkCSRFProtection();
 
  $job = $this->getRoute()->getObject();
  $job->publish();
 
  if ($cache = $this->getContext()->getViewCacheManager())
  {
    $cache->remove('sfJobeetJob/index?sf_culture=*');
    $cache->remove('sfJobeetCategory/show?id='.$job->getJobeetCategory()->getId());
  }
 
  $this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days')));
 
  $this->redirect($this->generateUrl('job_show_user', $job));
}

Кэш управляется классом sfViewCacheManager. Метод remove() удаляет кэш, связанный в внутренним URI. Для удаления кэша для всех возможных параметров переменной, используйте * как значение. Мы использовали sf_culture=* в коде выше, что означает, что Symfony будет удалять кэш для домашней страницы и на английском, и на французском.

Поскольку менеджер кэша равен null, когда кэш отключен, мы заключили удаление кэша в блок if.

Тестирование кэша

Перед тем, как мы начнем, нам нужно изменить конфигурацию для окружения test, чтобы включить слой кэширования:

# apps/frontend/config/settings.yml
test:
  .settings:
    error_reporting: <?php echo ((E_ALL | E_STRICT) ^ E_NOTICE)."\n" ?>
    cache:           true
    web_debug:       false
    etag:            false

Давайте протестируем страницу создания вакансии:

// test/functional/frontend/jobActionsTest.php
$browser->
  info('  7 - Job creation page')->
 
  get('/fr/')->
  with('view_cache')->isCached(true, false)->
 
  createJob(array('category_id' => Doctrine_Core::getTable('CategoryTranslation')->findOneBySlug('programming')->getId()), true)->
 
  get('/fr/')->
  with('view_cache')->isCached(true, false)->
  with('response')->checkElement('.category_programming .more_jobs', '/23/')
;

Тестер view_cache использован для тестирования кэша. Метод isCached() принимает два булевых значения:

  • Должна ли страница быть в кэше
  • Должен ли кэш быть с шаблоном

tip

Даже со всеми инструментами, предоставленными фреймворком функционального тестирования, иногда легче найти проблемы с помощью браузера. Это очень легко сделать. Просто создайте контроллер для окружения test. Журнал который хранится в log/frontend_test.log может также оказаться полезным.

Увидимся завтра!

Как и большинство другой функциональности Symfony, подфреймворк кэширования очень гибкий и позволяет разработчику настроить кэш на очень низком уровне.

Завтра мы поговорим о последнем шаге в жизненном цикле приложения: развертывании на промышленных серверах.

This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.