SymfonySeniorSystem design

Как реализовать кэширование в Symfony с помощью компонента Cache?

Symfony Cache реализован через PSR-6/PSR-16 адаптеры (Redis, Memcached, Filesystem и др.) и настраивается в framework.cache. Используйте CacheInterface, TagAwareCacheInterface или низкоуровневый CacheItemPoolInterface для управления записями.

Архитектура компонента Cache

Symfony Cache реализует PSR-6 (CacheItemPoolInterface) и PSR-16 (CacheInterface). Все адаптеры живут в пакете symfony/cache. Основные адаптеры:

  • RedisAdapter — для Redis/Redis Cluster через \Redis или \RedisCluster
  • FilesystemAdapter — кэш на диске, подходит для разработки
  • MemcachedAdapter — для Memcached
  • ArrayAdapter — in-memory, используется в тестах
  • ChainAdapter — последовательная цепочка из нескольких адаптеров (L1/L2 кэш)
  • TagAwareAdapter — обёртка для инвалидации по тегам

Конфигурация в config/packages/cache.yaml

framework:
  cache:
    app: cache.adapter.redis
    default_redis_provider: 'redis://localhost:6379'
    pools:
      cache.product_catalog:
        adapter: cache.adapter.redis
        default_lifetime: 3600
        tags: true
      cache.user_sessions:
        adapter: cache.adapter.filesystem
        default_lifetime: 86400

Использование через CacheInterface (PSR-16-like)

<?php

use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

class ProductService
{
    public function __construct(
        private readonly CacheInterface $cache,
    ) {}

    public function getProduct(int $id): array
    {
        return $this->cache->get(
            sprintf('product_%d', $id),
            function (ItemInterface $item) use ($id): array {
                $item->expiresAfter(3600);
                $item->tag(['product', sprintf('product_%d', $id)]);
                // имитируем запрос к БД
                return $this->fetchFromDatabase($id);
            }
        );
    }

    public function invalidateProduct(int $id): void
    {
        // TagAwareCacheInterface нужен для инвалидации по тегу
        if ($this->cache instanceof \Symfony\Contracts\Cache\TagAwareCacheInterface) {
            $this->cache->invalidateTags([sprintf('product_%d', $id)]);
        }
    }
}

Инъекция именованного пула

Когда нужен конкретный пул, а не cache.app, используйте атрибут #[Target] или явную привязку в сервисах:

# config/services.yaml
services:
  App\Service\CatalogService:
    arguments:
      $catalogCache: '@cache.product_catalog'
<?php

use Psr\Cache\CacheItemPoolInterface;

class CatalogService
{
    public function __construct(
        private readonly CacheItemPoolInterface $catalogCache,
    ) {}

    public function getCategories(): array
    {
        $item = $this->catalogCache->getItem('categories');
        if (!$item->isHit()) {
            $item->set($this->loadCategories());
            $item->expiresAfter(7200);
            $this->catalogCache->save($item);
        }
        return $item->get();
    }
}

ChainAdapter: L1 + L2 кэш

framework:
  cache:
    pools:
      cache.hot:
        adapters:
          - cache.adapter.array      # L1: in-process
          - cache.adapter.redis      # L2: Redis
        default_lifetime: 300

При промахе L1 Symfony автоматически заполняет его из L2, поэтому первый запрос в процессе обращается к Redis, а последующие — к памяти.

Инвалидация и команды CLI

# очистить весь пул приложения
php bin/console cache:pool:clear cache.app

# очистить именованный пул
php bin/console cache:pool:clear cache.product_catalog

# список всех пулов
php bin/console cache:pool:list

Подводные камни

  • TagAwareAdapter и Redis: нужен отдельный адаптер cache.adapter.redis_tag_aware; использование обычного cache.adapter.redis с тегами бросит исключение в рантайме.
  • Сериализация объектов: по умолчанию применяется igbinary или PHP serialize; объекты без __sleep/__wakeup могут кэшироваться с лишними полями или вовсе сломать десериализацию после деплоя.
  • Отравление кэша при деплое: если схема данных изменилась, старые записи возвращают невалидные данные; всегда инкрементируйте namespace или ключи версионирования.
  • cache.app vs cache.system: cache.system используется Symfony внутри (роутинг, аннотации); очищать его вручную опасно — он сбрасывается только через cache:clear.
  • ArrayAdapter в production: данные живут только в пределах одного HTTP-запроса; если пул случайно остаётся ArrayAdapter в prod-конфиге, кэш не работает.
  • Гонка при прогреве (stampede): несколько процессов одновременно вычисляют значение при промахе; используйте $item->beta(INF) для «раннего» продления или отдельный lock.
  • Lifetime = 0: в большинстве адаптеров означает «хранить вечно», а не «не кэшировать»; при ошибке конфигурации Redis постепенно заполняется до maxmemory.
  • Тесты: не подменяйте prod-пул вручную — объявите пул с адаптером cache.adapter.array в config/packages/test/cache.yaml, иначе интеграционные тесты читают/пишут в реальный Redis.

Common mistakes

  • Сводить cache к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
  • Не отделять validation, authorization, transaction boundary и business logic.
  • Не обсуждать idempotency, retries, shutdown и observability.

What the interviewer is testing

  • Объясняет cache через конкретную точку lifecycle в Symfony.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.
  • Связывает решение с метриками, backpressure, retry policy и graceful shutdown.

Sources

Related topics