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.