> For the complete documentation index, see [llms.txt](https://docs.johncms.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.johncms.com/10.0/obshie-svedeniya/caching.md).

# Кэширование

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

В JohnCMS кэш — это сервис `Johncms\Cache\CacheInterface`. Внутри работает [symfony/cache](https://symfony.com/doc/current/components/cache.html), но модуль об этом знать не должен: на каком хранилище стоит сайт, решает конфигурация, а код остаётся одинаковым.

Контракт основан на стандарте [PSR-16](https://www.php-fig.org/psr/psr-16/) и добавляет к нему три вещи: `remember()`, `rememberForever()` и `invalidateTags()`.

## Быстрый старт

Внедрите `CacheInterface` через конструктор:

```php
<?php

declare(strict_types=1);

namespace Johncms\Modules\MyModule\Application\Services;

use Johncms\Cache\CacheInterface;
use Johncms\Modules\MyModule\Domain\Repository\ArticleRepositoryInterface;

final readonly class PopularArticles
{
    /** Тег, под которым лежит всё кэшированное этим сервисом */
    private const CACHE_TAG = 'my-module';

    public function __construct(
        private CacheInterface $cache,
        private ArticleRepositoryInterface $repository,
    ) {
    }

    /**
     * @return array<int, string>
     */
    public function titles(): array
    {
        return $this->cache->remember(
            'my_module_popular',
            600,
            fn (): array => $this->repository->getPopularTitles(),
            [self::CACHE_TAG]
        );
    }
}
```

`remember()` — основной метод. Если значение в кэше есть, оно возвращается сразу; если нет — выполняется замыкание, результат сохраняется и возвращается. Обращения к базе не будет до тех пор, пока запись не истечёт или её не сбросят по тегу.

{% hint style="info" %}
Не пишите «проверить, потом посчитать, потом положить» вручную через `has()` и `set()`. Между проверкой и чтением запись может исчезнуть, и код получит `null` там, где ждал массив. `remember()` закрывает этот случай сам.
{% endhint %}

## Методы

| Метод                                                  | Что делает                                                                       |
| ------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `remember($key, $ttl, $callback, $tags)`               | Отдаёт значение из кэша, а на промахе считает его замыканием и сохраняет         |
| `rememberForever($key, $callback, $tags)`              | То же, но без срока: запись живёт, пока её не сбросят по тегу или не очистят кэш |
| `invalidateTags(...$tags)`                             | Помечает устаревшим всё, что лежит под этими тегами                              |
| `get($key, $default)`                                  | Значение или `$default`, если записи нет                                         |
| `set($key, $value, $ttl)`                              | Кладёт значение                                                                  |
| `delete($key)`                                         | Удаляет одну запись                                                              |
| `has($key)`                                            | Есть ли запись                                                                   |
| `clear()`                                              | Очищает весь кэш приложения                                                      |
| `getMultiple()` / `setMultiple()` / `deleteMultiple()` | То же самое пачкой                                                               |

Всё, кроме первых трёх строк, — стандарт PSR-16.

{% hint style="warning" %}
Методов `put()` и `forget()` нет. Класть значение — `set()`, удалять — `delete()`. Если вы переносите код со старых версий JohnCMS, где кэш был построен на Laravel, замените их; на каждое действие в контракте оставлено ровно одно имя.
{% endhint %}

### Срок жизни

Третий аргумент `remember()` — время жизни в секундах либо `DateInterval`. `null` означает «без срока» и равнозначен `rememberForever()`.

```php
$this->cache->remember('key', 600, $callback);                     // 10 минут
$this->cache->remember('key', new DateInterval('PT1H'), $callback); // час
$this->cache->rememberForever('key', $callback);                    // до сброса
```

Выбирайте срок по тому, насколько устаревшие данные допустимы на странице. Счётчик посетителей онлайн живёт десять секунд, счётчик сообщений форума — десять минут. Данные, которые меняются только по действию администратора, обычно кладут без срока и сбрасывают тегом.

## Теги: сброс группами

Тег — это метка на записи. Одна запись может нести несколько тегов, а сброс тега делает недействительными все записи под ним, сколько бы их ни было.

```php
// при записи
$this->cache->rememberForever('news_subsections', $callback, ['news']);
$this->cache->rememberForever('news_section_paths', $callback, ['news']);

// при изменении раздела — одна строка вместо перечисления ключей
$this->cache->invalidateTags('news');
```

Без тегов пришлось бы держать где-то список всех ключей и не забывать дополнять его при добавлении нового. С тегами достаточно пометить новую запись — метод сброса менять не нужно.

Правила:

* Тег называется по тому, **что** за данные, а не откуда они читаются: `news`, `counters`, `collections`, `my-module`.
* Держите тег константой рядом с ключами, которые он покрывает.
* Сбрасывайте теги там, где данные меняются, — в use case сохранения и удаления, а не в контроллере.

{% hint style="warning" %}
Сброс по тегу — не удаление. Записи сразу начинают читаться как промах, но место на диске освобождает команда `cache:pool:prune`, а не сам вызов. Это нормальная работа кэша, но об этом стоит помнить, когда смотрите на размер `data/cache`.
{% endhint %}

## Ключи и теги

Ключи и теги подчиняются ограничениям PSR-16: символы `{}()/\@:` запрещены и приводят к исключению `Psr\SimpleCache\InvalidArgumentException`. Буквы, цифры, точка, дефис и подчёркивание разрешены.

Начинайте ключ с имени модуля — `news_subsections`, `collections_code_map`, `my_module_popular`. Кэш общий на весь сайт, и одинаковые ключи из двух модулей затрут друг друга.

{% hint style="danger" %}
Никогда не подставляйте в ключ то, что ввёл посетитель, — логин, поисковый запрос, адрес страницы. При файловом хранилище ключ становится именем файла, и такой ключ виден в листинге каталога. Если ключ должен зависеть от введённого значения, хэшируйте его:

```php
$key = 'my_module_search_' . hash('xxh128', $query);
```

{% endhint %}

## Что можно класть в кэш

Массивы, скаляры и простые объекты-DTO.

{% hint style="danger" %}
Не кладите в кэш модели Eloquent. Модель сериализуется вместе со связями и состоянием, а после обновления CMS запись прочитается уже в изменённый класс. Приведите данные к массиву или DTO до того, как отдать их в кэш.

```php
// плохо
$this->cache->rememberForever('key', fn () => Article::query()->get());

// хорошо
$this->cache->rememberForever('key', fn () => Article::query()->get()
    ->map(fn (Article $article): array => ['id' => $article->id, 'name' => $article->name])
    ->all());
```

{% endhint %}

Замыкание должно возвращать значение всегда — в том числе когда данных нет. Пустой массив закэшируется и избавит от повторных запросов; `null` тоже допустим, но тогда `remember()` при каждом обращении будет считать заново.

## Настройка

Кэш настраивается в `config/autoload/cache.global.php`, а на конкретном сервере переопределяется через `cache.local.php` (см. [Конфигурационные файлы](/10.0/obshie-svedeniya/konfiguracionnye-faily-configs.md)).

```php
return [
    'cache' => [
        'driver'           => 'filesystem',
        'namespace'        => null,
        'default_lifetime' => 0,
        'directory'        => null,
        'tags_storage'     => 'auto',
        'redis'            => [
            'dsn' => 'redis://127.0.0.1:6379',
        ],
    ],
];
```

### Драйверы

| Драйвер      | Когда подходит                                                                        |
| ------------ | ------------------------------------------------------------------------------------- |
| `filesystem` | По умолчанию. Файлы в `data/cache/app`, работает на любом хостинге                    |
| `apcu`       | Быстрее файлового, но память принадлежит одному веб-серверу. Нужно расширение APCu    |
| `redis`      | Установка на нескольких серверах. Нужно расширение `redis` либо пакет `predis/predis` |
| `array`      | Только на время одного запроса. Предназначен для тестов                               |
| `null`       | Ничего не хранит, каждое чтение — промах. Удобно при отладке                          |

Теги работают на любом драйвере: даже те хранилища, которые сами тегов не умеют, оборачиваются так, чтобы `invalidateTags()` действовал.

{% hint style="warning" %}
Для `redis` политика вытеснения должна быть `noeviction` или `volatile-*`. При `allkeys-*` Redis может вытеснить служебную запись тега, и всё, что под ним лежало, останется висеть как действительное. На такой настройке драйвер откажется работать явной ошибкой, а не молча.
{% endhint %}

### Остальные параметры

* `namespace` — префикс, отделяющий этот сайт от других в общем хранилище. По умолчанию берётся версия CMS, поэтому после обновления кэш начинается с чистого листа и старые записи не читаются.
* `default_lifetime` — срок для записей, которые его не задали. `0` — без ограничения.
* `directory` — где хранит файлы драйвер `filesystem`. `null` — `data/cache/app`.
* `tags_storage` — как файловый драйвер связывает теги с записями: `auto` (по умолчанию), `symlink` или `files`. В режиме `auto` система один раз проверяет, разрешает ли хостинг символические ссылки, и запоминает ответ. Менять это нужно только если проверка ошиблась — например, на хостинге, где симлинки создаются, но не работают.

## Консольные команды

| Команда                          | Что делает                                                                                    |
| -------------------------------- | --------------------------------------------------------------------------------------------- |
| `cache:pool:clear`               | Очищает кэш приложения — то, что накэшировали модули                                          |
| `cache:pool:clear news counters` | Сбрасывает только записи с указанными тегами                                                  |
| `cache:pool:prune`               | Освобождает место, занятое истёкшими и сброшенными записями                                   |
| `cache:clear`                    | Полностью очищает `data/cache`: кэш приложения, скомпилированный контейнер, маршруты, шаблоны |

```bash
php system/bin/console cache:pool:clear
php system/bin/console cache:pool:clear news
php system/bin/console cache:pool:prune
```

`cache:pool:prune` уже стоит в [планировщике](/10.0/konsol/planirovshchik-zadach-schedule.md) и выполняется ночью. Если планировщик на сайте не настроен, запускайте её вручную — иначе каталог `data/cache` будет расти.

Обе команды доступны и через [задачи обслуживания в админке](/10.0/konsol/zadachi-obsluzhivaniya-v-adminke.md), без доступа к консоли.

{% hint style="info" %}
После обновления CMS используйте `cache:clear` — он сбрасывает в том числе скомпилированный контейнер и шаблоны. Для повседневного «сбросить данные модулей» достаточно `cache:pool:clear`, который всё это не трогает.
{% endhint %}

## Тесты

Для тестов есть настоящий кэш в памяти — `Tests\Support\InMemoryCache`:

```php
use Tests\Support\InMemoryCache;

$cache = InMemoryCache::create();
$service = new PopularArticles($cache, $repository);
```

Не подменяйте `CacheInterface` моком: мок метода `remember()` подтверждает лишь то, что метод вызвали, но не то, что запись получила тег и сбросится вместе с остальными. С настоящим кэшем проверяется поведение — сохранилось, сбросилось, посчиталось заново.

## Чего делать не нужно

* **Перечислять ключи в методе очистки.** Для этого есть теги: новый ключ достаточно пометить.
* **Кэшировать то, что зависит от текущего посетителя,** — права, содержимое корзины, персональные списки, — без включения идентификатора в ключ. Кэш общий на весь сайт, и один посетитель увидит данные другого.
* **Заводить свой файловый кэш** (`file_put_contents` в `data/cache`). Такой кэш не участвует ни в очистке, ни в сбросе по тегу, и его не видно ни одной из команд.
* **Оборачивать в кэш дешёвый запрос.** Чтение записи по первичному ключу обходится дешевле, чем чтение файла кэша и распаковка значения.
