Перейти к содержанию

Кэш

Пространство имён Keel\Cache. PSR-16 кэш без зависимостей с тремя драйверами, подключённый как Psr\SimpleCache\CacheInterface; symfony/cache или любая другая реализация PSR-16 подменяется алиасом интерфейса.

// config/cache.php
return static fn(Env $env, string $projectDir): CacheConfig => new CacheConfig(
    driver: 'auto',                           // apcu, если загружен, иначе file; либо "apcu", "file", "array"
    directory: $projectDir . '/var/cache/data',
    prefix: 'myapp:',
    defaultTtl: 3600,                         // секунды для set() без TTL; null = навсегда
);

// config/services.php
return new ServicesConfig([CacheProvider::class]);
final class RatesService
{
    public function __construct(private readonly CacheInterface $cache, private readonly ClientInterface $http) {}

    public function rate(string $currency): float
    {
        $rate = $this->cache->get('rate.' . $currency);
        if (!is_float($rate)) {
            $rate = $this->fetch($currency);
            $this->cache->set('rate.' . $currency, $rate, 600);
        }

        return $rate;
    }
}

Драйверы

Драйвер Общий для Примечания
ApcuCache всех процессов php-fpm или потоков FrankenPHP сервера самый быстрый; очищается при рестарте; нужен ext-apcu с apc.enable_cli=1 для консольных команд
FileCache всех воркеров хоста один сериализованный файл на запись, атомарная запись, просроченные удаляются при чтении и вероятностной уборкой (sweep() запускает её вручную и заодно убирает временные файлы, оставшиеся от упавшей записи)
ArrayCache только текущего процесса тесты и мемоизация в пределах воркера
RedisCache (driver: 'redis') всех воркеров на всех хостах ext-redis через RedisProvider (ниже); значения сериализуются serialize(), так что держите инстанс приватным
MemcachedCache (driver: 'memcached') всех воркеров на всех хостах ext-memcached, servers: ['host:11211']; ключи длиннее 250 байт хэшируются; clear() сбрасывает весь инстанс

Ключи по PSR-16: до 256 символов, без {}()/\@:; плохой ключ бросает InvalidKeyException (это Psr\SimpleCache\InvalidArgumentException). TTL — секунды или DateInterval; неположительный TTL удаляет. Сохранённый null считается попаданием.

Каталог FileCache. Записи десериализуются с разрешением любых классов, поэтому тот, кто может писать в каталог, может подсунуть объекты в ваш процесс. Драйвер создаёт отсутствующий каталог с правами 0700 и отказывается (RuntimeException) от каталога, который является символической ссылкой, доступен на запись всем или доступен на запись группе, но принадлежит другому пользователю. Без directory по умолчанию берётся предсказуемый путь в системном временном каталоге: это нормально на хосте, где работает только ваше приложение, а на общей машине задайте directory — приватный путь вроде $projectDir . '/var/cache/data'.

Keel\Cache\Cache (алиас PSR-интерфейса) добавляет remember($key, $ttl, fn() => ...): значение из кэша или результат замыкания, сохранённый под ключом. Сохранённый null — попадание.

Redis

RedisConfig в config/redis.php плюс RedisProvider дают контейнеру соединение \Redis (ext-redis, подключается при первом обращении, постоянное между запросами воркера), проверку здоровья redis для /health и, по use, хранилища Keel, которые иначе живут в APCu или памяти процесса:

// config/redis.php
return static fn(Env $env): RedisConfig => RedisConfig::fromUrl(
    $env->require('REDIS_URL'),               // redis://:password@host:6379/2, rediss:// для TLS
    prefix: 'myapp:',
    use: ['sessions', 'counters', 'metrics'],
);
// или new RedisConfig(host: '127.0.0.1', port: 6379, auth: null, database: 0, prefix: 'myapp:', use: [...])

// config/cache.php
return new CacheConfig(driver: 'redis', prefix: 'myapp:');
use Регистрирует Заменяет
sessions Keel\Session\RedisStorage как SessionStorageInterface файловые или cookie-сессии: общие для всех узлов, истекают средствами Redis, продлеваются как файловые
counters Keel\RateLimit\RedisCounters как CounterStorage счётчики лимитов в APCu: одна корзина на весь парк; при недоступности Redis закрывается (fail closed)
metrics Keel\Metrics\RedisStorage как MetricsStorage метрики в APCu: один hash на парк, так что /metrics на любом узле отдаёт всё

Каждый ключ начинается с префикса (myapp:cache:, myapp:sess:, myapp:rl:, myapp:metrics). RedisProvider::connect() создаёт соединение; зарегистрируйте RedisConfig до загрузки провайдера (конфиги регистрируются первыми, так что config/redis.php достаточно).

Кэш вывода

#[OutputCache] отдаёт ответ маршрута целиком из кэша: хэндлер выполняется один раз, следующие GET и HEAD получают сохранённую копию, пока она не истечёт или пока не сбросят один из её тегов. Нужен PSR-16 CacheInterface (CacheProvider).

#[Get('/products/{id}')]
#[OutputCache(ttl: 300, tags: ['products', 'product:{id}'], varyByQuery: ['lang'], varyByHeaders: ['Accept-Language'])]
public function show(int $id): ProductView { /* ... */ }

#[Patch('/products/{id}')]
public function update(int $id, #[FromBody] ProductPatch $patch, OutputCacheStore $pages): ProductView
{
    $product = $this->products->update($id, $patch);
    $pages->invalidateTags('products', "product:$id");

    return ProductView::from($product);
}

Ключ — метод, хост и путь, плюс query (varyByQuery: true по умолчанию; false его игнорирует; список оставляет только эти параметры), перечисленные заголовки запроса и, с varyByPrincipal: true, principal. Теги могут ссылаться на плейсхолдеры маршрута (product:{id}). invalidateTags() выдаёт каждому тегу новую версию; сохранённый ответ помнит версии, с которыми он записан, и устаревает, как только одна из них сменилась, — это работает на любом PSR-16 хранилище без перебора ключей. Версии тегов хранятся без срока, а вытесненная версия читается как неизвестная, так что её ответы становятся устаревшими, а не снова свежими.

Ответы получают X-Cache: HIT или MISS (OutputCacheConfig::$header, null — без заголовка) и при попадании — Age. Сохраняются только ответы 200; никогда — те, что ставят cookie, говорят Cache-Control: no-store или private, стримят тело или больше OutputCacheConfig::$maxBodyBytes (1 МиБ). Запросы с заголовком Authorization идут мимо кэша, кроме случая, когда маршрут варьируется по principal и principal к этому моменту уже известен: объявляйте #[Auth] раньше #[OutputCache]. Без этого такие запросы идут мимо кэша, а не делят одну копию. В отличие от #[CacheControl], который говорит клиентам и прокси, что хранить, этот кэш экономит работу на сервере.

Worker mode

ArrayCache — кэш на воркер: при четырёх воркерах запись вычислится до четырёх раз, а clear() дойдёт до одного из них. Для всего, что должно быть согласовано между воркерами, используйте APCu или файлы, а ArrayCache — для данных, которые дёшево пересобрать.

У RateLimit и Metrics свои хранилища с тем же делением APCu/память; через этот кэш они не ходят.