Кэширование данных заключается в сохранении некоторой переменной 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. Всё, о чем не упомянуто здесь, вы можете найти по ссылкам выше.