Yii2: Кэширование данных (примеры)
Кэширование данных заключается в сохранении некоторой переменной PHP в кэше и её последующем извлечении. Оно является основой для расширенных возможностей, таких как кэширование запросов и кэширование страниц.
Кэширование данных (Полное руководство по Yii 2.0) здесь
Abstract Class yii\caching\Cache
(API Documentation) здесь
По-умолчанию кэш в Yii2 (Advanced
) разделён и сохраняется в соответствующие каталоги: backend\runtime\cache
и frontend\runtime\cache
. Убедиться в этом можно, вызвав команду Yii::$app->cache
из frontend или backend, соответственно.
Для того, чтобы иметь возможность работать с кэшем другого приложения (например из backend удалить кэш frontend) можно указать для этих приложений общее хранилище. Весь кэш приложений будем хранить в каталоге common
, затем будем удалять нужный нам кэш (в том числе кэш frontend) из админки.
В файле common\config\main.php
:
'components' => [ 'cache' => [ 'class' => 'yii\caching\FileCache', // Используем хранилище yii\caching\FileCache 'cachePath' => '@common/runtime/cache' // Храним кэш в common/runtime/cache ] ]
В приложении одновременно можно использовать разные способы хранения кэша. Об остальных поддерживаемых хранилищах читайте в документации, я же буду использовать (для примера) только yii\caching\FileCache
.
Кэширование произвольных данных (результаты выборки, данные внутри виджетов и тп.)
// Обращаемся к кэшу приложения $cache = Yii::$app->cache; // Формируем ключ $key = 'category_list'; // Пробуем извлечь категории из кэша. $categories = $cache->get($key); if ($categories === false) { //Если $categories нет в кэше, вычисляем заново $categories = Category::find()->all(); // Сохраняем значение $categories в кэше по ключу. Данные можно получить в следующий раз. $cache->set($key, $categories); } print_r($categories);
Начиная с версии 2.0.11, компонент кэширования предоставляет метод getOrSet()
, который упрощает код при получении, вычислении и сохранении данных. Приведённый ниже код делает в точности то же самое, что и код в предыдущем примере:
// Обращаемся к кэшу приложения $cache = Yii::$app->cache; // Формируем ключ $key = 'category_list'; // Данный метод возвращает данные либо из кэша, либо из откуда-либо и записывает их в кэш по ключу на 1 час $categories = $cache->getOrSet($key, function () { return Category::find()->all(); }, 3600); print_r($categories);
Кэширование фрагментов
Кэширование фрагментов относится к кэшированию фрагментов страницы. Обычно кэширование фрагментов используйтся в представлении:
<div class="site-index"> <?php if($this->beginCache('category_list', ['duration' => 3600])):?> <? foreach($categories as $category): ?> <p><?= $category->name ?></p> <? endforeach; ?> <? $this->endCache(); endif; ?> </div>
Срок хранения duration
. По умолчанию, если срок хранения не указан $defaultDuration = 0
. Это означает то, что время хранения бесконечен.
// Кэшированние содержимого на бесконечный срок. Если нужен срок хранения кэша, то установить нужное значение в секундах (например, 3600 = 1 час) if ($this->beginCache($id, ['duration' => 0])) { // ... здесь содержимое ... $this->endCache(); }
Зависимости dependency
:
// Отображение содержимого зависит от того, изменена или нет категория (изменения значения столбца updated_at) $dependency = [ 'class' => 'yii\caching\DbDependency', 'sql' => 'SELECT MAX(updated_at) FROM category', ]; if ($this->beginCache($id, ['dependency' => $dependency])) { // ... здесь содержимое ... $this->endCache(); }
Вариации variations
на примере кэширования виджета категорий:
<? //В variations нужно передать id текущей категории (для каждой категории менюшка сохраняется в отдельный параметр кэша) ?> <?php if($this->beginCache('aside-widget', [ 'duration' => 3600, //(default = 0) 'dependency' => [ 'class' => 'yii\caching\DbDependency', 'sql' => 'SELECT MAX(update_at) FROM ' . Category::tableName(), ], 'variations' => [ isset($this->params['category']) ? $this->params['category']->id : null ] ])): ?> <?= CategoryWidget::widget() ?> <?php $this->endCache(); endif; ?>
Кэширование запросов
Кэширование запросов - это специальная функция, построенная на основе кэширования данных. Она предназначена для кэширования результатов запросов к базе данных.
// Кэширование запросов для DAO // Возвращает ассоциативный массив с именами столбцов и значений $categories = Yii::$app->db->cache(function () { return Yii::$app->db->createCommand('SELECT * FROM `category`')->queryAll(); }); // Кэширование запросов для ActiveRecord (на 1 час) // Возвращает объект $categories = Category::getDb()->cache(function (){ return Category::find()->all(); }, 3600);
В пределах cache()
вы можете отключить кэширование запроса. В этом случае вы можете использовать [[yii\db\Connection::noCache()]]
:
categories = Yii::$app->db->cache(function () { // SQL запросы, которые используют кэширование Yii::$app->db->noCache(function ($db) { // SQL запросы, которые не используют кэширование }); return $categories; });
Если вы просто хотите использовать кэширование для одного запроса, вы можете вызвать [[yii\db\Command::cache()]]
при построении команды:
// использовать кэширование запросов и установить срок действия кэша на 1 час $categories = $db->createCommand('SELECT * FROM category)->cache(3600)->queryAll();
Можете использовать [[yii\db\Command::noCache()]]
для отключения кэширования запросов для одной команды:
$result = $db->cache(function ($db) { // ... Используется кэширование SQL запросов ... // не использовать кэширование запросов для этой команды $categories = $db->createCommand('SELECT * FROM category)->noCache()->queryAll(); // ... return $result; });
Кэширование страниц
Кэширование страниц — это кэширование всего содержимого страницы на стороне сервера. Когда эта страница будет запрошена, сервер вернет её из кэша вместо того чтобы генерировать её заново.
public function behaviors() { return [ [ 'class' => 'yii\filters\PageCache', 'only' => ['view'], 'duration' => 3600, 'variations' => [ Yii::$app->language, ], 'dependency' => [ 'class' => 'yii\caching\DbDependency', 'sql' => 'SELECT MAX(update_at) FROM ' . Page::tableName(), ] ] ]; }
Приведённый код задействует кэширование только для действия view
. Содержимое страницы кэшируется максимум на 1 час и варьируется в зависимости от текущего языка приложения. Кэшированная страница должна быть признана просроченной, если поле update_at
изменилось.
Кэширование страниц очень похоже на кэширование фрагментов. В обоих случаях поддерживаются параметры duration
(продолжительность), dependencies
(зависимости), variations
(вариации), и enabled
(включен). Главное отличие заключается в том, что кэширование страницы реализовано в виде фильтра действия, а кэширование фрагмента в виде виджета.
Очистка кэша
Для очистки всего кэша, вы можете вызвать [[yii\caching\Cache::flush()]]
.
Очистить кэш из консоли можно вызвав yii cache/flush
.
yii cache
: отображает список доступных кэширующих компонентов приложенияyii cache/flush cache1 cache2
: очищает кэш в компонентах cache1, cache2 (можно передать несколько названий компонентов кэширования, разделяя их пробелом)yii cache/flush-all
: очищает кэш во всех кэширующих компонентах приложения
Внимание!
Консольное приложение использует отдельный конфигурационный файл по умолчанию. Для получения должного результата, убедитесь, что в конфигурациях консольного и веб-приложения у вас одинаковые компоненты кэширования.
Очистить кэш из контроллера:
// Очитит кэш всего приложения Yii::$app->cache->flush();
Очистить кэш фрагмента по ключу:
function deleteFragmentCacheByKey($key) { return Yii::$app->cache->delete(['yii\widgets\FragmentCache', $key]); }
Автоматическое удаление кэша при обновлении записи в базе данных
Когда в БД вносятся изменения (добавление, редактирование, удалении записи в админке), данные в кэше становятся неактуальными. В этом случае можно к модели, которая отвечает за нужную таблицу подключить поведение с событиями для ActiveRecord (EVENT_AFTER_INSERT
, EVENT_AFTER_UPDATE
, EVENT_AFTER_DELETE
), которые при каждом срабатывании будут удалять нужный кэш.
файл CachedBehavior.php
в папке common\components\behaviors
:
<?php namespace common\components\behaviors; use Yii; use yii\base\Behavior; use yii\db\ActiveRecord; class CachedBehavior extends Behavior { public $cache_key; public function events() { return [ ActiveRecord::EVENT_AFTER_INSERT => 'deleteCache', ActiveRecord::EVENT_AFTER_UPDATE => 'deleteCache', ActiveRecord::EVENT_AFTER_DELETE => 'deleteCache', ]; } public function deleteCache() { foreach ($this->cache_key as $id){ Yii::$app->cache->delete($id); } } }
Подключаем поведение к нужной модели:
public function behaviors() { return [ 'CachedBehavior' => [ 'class' => CachedBehavior::class, 'cache_key' => ['category_list'], ] ]; }
Автоматическое удаление кэша при выполнении действий контроллера
Код, указанный выше обновляет кэш при действиях, указаных в поведении CachedBehavior
. Данное поведение привязано к модели, которая в свою очередь занимается ещё выборкой записей. Такой подход в данном случае не очень интнресный, поэтому мы воспользуемся методом удаления кэша из контроллера. Плюсом такого метода является то, что мы можем на нужный нам контроллер навесить нужные события и в конечном итоге модель будет заниматься только логикой (выборка данных и тд), а контроллер по событию каких-либо действий с записями удалять нужный кэш.
Файл common\components\behaviors\DeleteCacheBehavior.php
:
<?php namespace common\components\behaviors; use Yii; use yii\base\Behavior; use yii\web\Controller; class DeleteCacheBehavior extends Behavior { public $cache_key; public $actions; public function events() { return [ Controller::EVENT_BEFORE_ACTION => 'deleteCache', ]; } public function deleteCache() { $action = Yii::$app->controller->action->id; //название текущего действия if(array_search($action, $this->actions)=== false) return; Foreach ($this->cache_key as $id){ Yii::$app->cache->delete($id); } } }
Добавляем поведение в контроллер (у меня это backend\controllers\CategoryController.php
):
public function behaviors() { return [ // ... //Класс удаление кэша при выполнении указанных действий [ 'class' => DeleteCacheBehavior::class, 'cache_key' => ['category_list'], 'actions' => ['create', 'update', 'delete'], ], // ... ]; }
Удаление кэша по тэгу
Class yii\caching\TagDependency
(API Documentation) здесь
При кэшировании данных мы указываем зависимость:
$cache = Yii::$app->cache; $key = 'category_list'; $categories = $cache->getOrSet($key, function () { return Category::find()->all(); }, 3600, new TagDependency([ 'tags' => 'category_list' ]));
В нужном нам контроллере и нужном экшене (после сохранения модели):
if ($model->load(Yii::$app->request->post()) && $model->save()) { // При обновлении записи обновится кэш по указанному тэгу TagDependency::invalidate(Yii::$app->cache, 'category_list'); return $this->redirect(['view', 'id' => $model->id]); }
На гитхабе нашёл вот такой trait. Сам не пробовал использовать. Кому интересно, посмотрите.
Простой сервис для удаления кэша по тэгу
Пример очень простого сервиса по по очистке кэша по тэгу. Дело в том, что лучше бы не вызывать постоянно $cache = Yii::$app->cache;
, а работать с объектом кэша напрямую через некий сервис, который будет в нужный нам момент очищать кэш по тэгу.
<?php namespace core\services; use yii\caching\Cache; use yii\caching\TagDependency; class CacheService { public $cache; public function __construct(Cache $cache) { $this->cache = $cache; } public function deleteTag($tag) { TagDependency::invalidate($this->cache, $tag); } public function flush() { $this->cache->flush(); } }
Там где получаем данные (модель или лучше отдельный репозиторий для получения данных):
<?php namespace core\readModels\Blog; use core\entities\Blog\Category; use core\services\CacheService; use yii\caching\TagDependency; class CategoryReadRepository { private $cacheService; public function __construct(CacheService $cacheService) { $this->cacheService = $cacheService; } public function getAll(): array { $key = Category::CACHE_ASIDE; $query = Category::find()->select(['name', 'slug'])->orderBy(['name' => SORT_ASC]); return $this->cacheService->cache->getOrSet($key, function () use ($query) { return $query->all(); }, 0, new TagDependency([ 'tags' => Category::CACHE_ASIDE ])); } }
Обертка для кэширования данных в Yii2
Можно кэширование обернуть в функцию и избавить себя от многократного писания переменных $data, $cache, $key. Тем более, если рядом используется кэширование нескольких фрагментов данных.
<?php namespace common\components; use Yii; use yii\helpers\Url; use yii\helpers\Html; class Controller extends \yii\web\Controller { /** * Data caching * @param string $cacheId Cache ID * @param \Closure $query Data to cache * @param \yii\caching\Dependency $dependency Cache dependency * @param int $cacheTime Duration in seconds * @return mixed Output cached model data */ protected function cache($cacheId, \Closure $query, \yii\caching\Dependency $dependency = null, $cacheTime = null) { $cacheTime = ($cacheTime === null) ? Yii::$app->option->get('cache_time') : $cacheTime; $data = Yii::$app->cache->get($cacheId); if ($data === false) { $data = $query(); Yii::$app->cache->set($cacheId, $data, $cacheTime, $dependency); } return $data; } } ?> // === Использование <?php namespace frontend\controllers; use Yii; use yii\helpers\Html; use yii\caching\DbDependency; use yii\web\NotFoundHttpException; use frontend\models\News\News; class NewsController extends \frontend\components\Controller { /** * Show single news item * @param string $title News url * @return \yii\web\View * @throws \yii\web\NotFoundHttpException */ public function actionShow($title) { $title = Html::encode($title); $news = $this->cache( 'news_' . Yii::$app->language . '_t' . md5($title), function() use ($title) { return News::find() ->with(['newsTranslates' => function($query) { $query->andWhere(['lang' => Yii::$app->language]); }]) ->show($title) ->asArray() ->one(); }, new DbDependency(['sql' => 'SELECT `lastModified` FROM `' . News::tableName() . '` WHERE `alias`="' . $title . '"']) ); return $this->render('show', compact('news')); } }
Здесь я кратким конспектом рассмотрел основные принципы кэширования в Yii2. Всё, о чем не упомянуто здесь, вы можете найти по ссылкам выше.