Symfony Cache : PSR-6, PSR-16, tags, adapters
Le cache est le levier de performance le plus impactant dans une application Symfony. Que ce soit pour eviter des requetes SQL couteuses, accelerer la serialisation ou servir des reponses HTTP sans executer de code PHP, le composant Cache de Symfony offre une solution complete basee sur les standards PSR-6 et PSR-16. Ce guide couvre la configuration, les adapters, l'invalidation par tags et les strategies de cache en production.
PSR-6 vs PSR-16 : deux interfaces, un objectif
Symfony implemente deux standards PHP pour le cache :
PSR-6 : CacheItemPoolInterface
L'interface PSR-6 utilise un pattern pool/item. Vous recuperez un item du pool, verifiez s'il est en cache, le modifiez et le sauvegardez :
<?php
use Psr\Cache\CacheItemPoolInterface;
class ProductService
{
public function __construct(
private readonly CacheItemPoolInterface $cache,
) {}
public function getExpensiveData(int $productId): array
{
$cacheItem = $this->cache->getItem('product_' . $productId);
if ($cacheItem->isHit()) {
return $cacheItem->get();
}
// Calcul couteux
$data = $this->computeExpensiveData($productId);
$cacheItem->set($data);
$cacheItem->expiresAfter(3600); // 1 heure
$this->cache->save($cacheItem);
return $data;
}
}
Symfony Cache Contracts
Symfony propose aussi ses propres contracts de cache qui simplifient l'usage courant. L'interface CacheInterface (a ne pas confondre avec PSR-16) utilise un callback pour eviter le pattern get/check/set :
<?php
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class ProductService
{
public function __construct(
private readonly CacheInterface $cache,
) {}
public function getExpensiveData(int $productId): array
{
return $this->cache->get('product_' . $productId, function (ItemInterface $item): array {
$item->expiresAfter(3600);
// Ce callback n'est execute que si le cache est manquant
return $this->computeExpensiveData($productId);
});
}
}
Les contracts cache incluent aussi le mecanisme de cache stampede protection : quand plusieurs requetes arrivent simultanement et que le cache est expire, seule la premiere execute le callback. Les autres attendent le resultat. Cela evite la surcharge du backend.
Configuration des pools de cache
Symfony permet de configurer plusieurs pools de cache avec des adapters differents :
# config/packages/cache.yaml
framework:
cache:
# Pool par defaut
app: cache.adapter.redis
system: cache.adapter.apcu
# Pools personnalises
pools:
cache.product:
adapter: cache.adapter.redis
default_lifetime: 3600
cache.api_responses:
adapter: cache.adapter.filesystem
default_lifetime: 600
cache.sessions:
adapter: cache.adapter.redis
provider: 'redis://secret@redis-sessions:6379/2'
cache.tagged:
adapter: cache.adapter.redis
tags: true # Active le support des tags
Chaque pool est un service injectable. Utilisez l'autowiring avec le nom du parametre :
<?php
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
class CatalogService
{
public function __construct(
private readonly CacheInterface $cacheProduct, // Injecte cache.product
private readonly TagAwareCacheInterface $cacheTagged, // Injecte cache.tagged
) {}
}
Adapters disponibles
Symfony fournit des adapters pour les backends de cache les plus courants :
Filesystem (defaut)
Stocke les items dans des fichiers sur le disque. Aucune dependance externe. Adapte au developpement et aux petites applications.
Redis
Le choix recommande en production. Rapide, supporte les tags, les TTL precis et la persistance :
# .env
REDIS_URL="redis://localhost:6379"
# config/packages/cache.yaml
framework:
cache:
app: cache.adapter.redis
default_redis_provider: '%env(REDIS_URL)%'
Memcached
Performant pour le cache distribue, mais ne supporte pas nativement les tags. Symfony emule les tags via un mecanisme supplementaire :
# .env
MEMCACHED_DSN="memcached://localhost:11211"
# config/packages/cache.yaml
framework:
cache:
app: cache.adapter.memcached
default_memcached_provider: '%env(MEMCACHED_DSN)%'
APCu
Cache en memoire partagee du processus PHP. Extremement rapide car il n'y a aucun round-trip reseau. Ideal pour les donnees de configuration ou les traductions :
framework:
cache:
system: cache.adapter.apcu
Invalidation par tags
L'invalidation par tags est la fonctionnalite la plus puissante du cache Symfony. Elle permet de supprimer des groupes d'items caches sans connaitre leurs cles individuelles :
<?php
use Symfony\Contracts\Cache\TagAwareCacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class CatalogService
{
public function __construct(
private readonly TagAwareCacheInterface $cache,
) {}
public function getProduct(int $id): array
{
return $this->cache->get("product_{$id}", function (ItemInterface $item) use ($id): array {
$item->expiresAfter(7200);
$item->tag(['products', "product_{$id}", 'catalog']);
return $this->repository->find($id)->toArray();
});
}
public function getCategoryProducts(int $categoryId): array
{
return $this->cache->get("category_{$categoryId}", function (ItemInterface $item) use ($categoryId): array {
$item->expiresAfter(7200);
$item->tag(['products', "category_{$categoryId}", 'catalog']);
return $this->repository->findByCategory($categoryId);
});
}
public function onProductUpdate(int $productId): void
{
// Invalide uniquement le produit modifie
$this->cache->invalidateTags(["product_{$productId}"]);
}
public function onCatalogImport(): void
{
// Invalide tout le catalogue (produits + categories)
$this->cache->invalidateTags(['catalog']);
}
}
L'invalidation par tags est atomique et performante avec Redis. Elle evite la purge complete du cache et permet une granularite fine.
Cache HTTP : la couche la plus efficace
Le cache HTTP evite completement l'execution de code PHP. Symfony simplifie sa configuration avec l'attribut #[Cache] :
<?php
use Symfony\Component\HttpKernel\Attribute\Cache;
class ArticleController extends AbstractController
{
#[Cache(maxage: 3600, public: true, smaxage: 86400)]
#[Route('/articles/{slug}', name: 'app_article_show')]
public function show(string $slug): Response
{
// Headers generes :
// Cache-Control: public, max-age=3600, s-maxage=86400
$article = $this->articleRepository->findBySlug($slug);
return $this->render('article/show.html.twig', ['article' => $article]);
}
#[Route('/api/articles', name: 'api_articles')]
public function apiList(): Response
{
$articles = $this->articleRepository->findAll();
$response = $this->json($articles);
// Configuration manuelle avec ETag
$response->setEtag(md5($response->getContent()));
$response->setPublic();
$response->setMaxAge(600);
$response->isNotModified($this->container->get('request_stack')->getCurrentRequest());
return $response;
}
}
Pour un reverse proxy comme Varnish, le header s-maxage controle la duree de cache cote serveur, independamment du max-age envoye au navigateur.
Cache warming : preparer le cache au deploiement
Symfony inclut un mecanisme de cache warming qui pre-genere les caches critiques lors du deploiement :
# Warm up du cache (inclus dans cache:clear par defaut)
php bin/console cache:warmup --env=prod
# Creer un cache warmer personnalise
<?php
namespace App\Cache;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
class ProductCacheWarmer implements CacheWarmerInterface
{
public function isOptional(): bool
{
return true; // Peut etre ignore si le warmup est trop long
}
public function warmUp(string $cacheDir, ?string $buildDir = null): array
{
// Pre-charger les donnees les plus consultees
$this->productService->warmUpTopProducts();
return [];
}
}
Doctrine result cache
Doctrine ORM peut utiliser les pools de cache Symfony pour mettre en cache les resultats de requetes DQL :
# config/packages/doctrine.yaml
doctrine:
orm:
metadata_cache_driver:
type: pool
pool: doctrine.system_cache_pool
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.adapter.redis
doctrine.system_cache_pool:
adapter: cache.adapter.apcu
// Utilisation dans un repository
$query = $this->createQueryBuilder('p')
->where('p.active = :active')
->setParameter('active', true)
->getQuery();
$query->enableResultCache(3600, 'active_products');
$products = $query->getResult();
Strategies de cache en production
Apres avoir deploye des applications Symfony en production pour de nombreux clients, voici les strategies que je recommande :
- Couche 1 - Cache HTTP : Varnish ou CDN devant votre application. C'est la couche la plus efficace
- Couche 2 - Cache applicatif : Redis pour les resultats de requetes, les calculs couteux et les sessions
- Couche 3 - Cache systeme : APCu pour les metadonnees Doctrine, le routage compile et les traductions
- Invalidation : utilisez les tags pour invalider par domaine metier, pas par cle individuelle
- Monitoring : surveillez le hit ratio de chaque pool. Un ratio inferieur a 80% indique un probleme de TTL ou d'invalidation trop frequente
Pour consommer des APIs externes avec mise en cache, consultez notre guide sur le HTTP Client Symfony. Si vous utilisez le Serializer, decouvrez comment le combiner avec le cache dans notre guide complet du Serializer. Et pour la mise en production, notre checklist de deploiement couvre la configuration du cache en environnement reel.
Besoin d'optimiser les performances de votre application Symfony ? Consultez mes tarifs, explorez mes services, ou contactez-moi pour un audit performance.
