Symfony11 min

Symfony Cache : PSR-6, PSR-16, tags, adapters

Par Pierre-Arthur Demengel
SymfonyCachePerformanceRedis

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.

Questions fréquentes

13 projets livrésGrand-Est & BelgiqueLighthouse >90Disponible immédiatement

Un projet en tête ?

Discutons de votre site web. Réponse garantie sous 24h.

Ou appelez directement :06 95 41 30 25

WhatsApp
Appeler