Skip to Content
SymfonyPartie 15 — Expert : perf, prod, architecture15.1 — Cache : HTTP, composant, Redis

Chapitre 15.1 — Cache : HTTP, composant, Redis

Partie 15 : Expert — Chapitre 1/9 Version de référence : Symfony 7.4.

⏱️ TL;DR

  • Plusieurs niveaux de cache, du plus « haut » au plus « bas » : HTTP (reverse proxy / CDN / Varnish), application (composant Cache, PSR-6/16), OPcache (bytecode PHP), Doctrine (cache de second niveau).
  • Cache HTTP : des en-têtes (Cache-Control, ETag) permettent à un proxy/CDN de servir une réponse sans toucher à PHP. Le plus rentable pour du contenu public.
  • Composant Cache : CacheInterface::get($key, $callback) mémorise le résultat d’un calcul coûteux ; les tags permettent l’invalidation ciblée. Adaptateurs : filesystem, Redis, APCu.
  • Redis sert le cache et les sessions en production multi-serveurs.
  • « Il n’y a que deux problèmes difficiles : l’invalidation de cache et nommer les choses. » Utilisez TTL + tags.
  • Pour un dev Next.js : cache HTTP ≈ le cache CDN/revalidate de Next ; le composant Cache ≈ un unstable_cache/Redis côté serveur.

🎯 Objectifs

  • Situer les niveaux de cache et savoir lequel utiliser.
  • Poser des en-têtes de cache HTTP.
  • Mémoriser des calculs avec le composant Cache et invalider par tags.
  • Configurer Redis pour cache et sessions.

1. Les niveaux de cache

Règle : cachez le plus haut possible. Une réponse servie par le CDN ne consomme aucune ressource serveur. Un résultat mémorisé par le composant Cache évite un calcul/une requête. On combine les niveaux selon la nature de la donnée.

2. Le cache HTTP

Le plus rentable pour du contenu public/peu variable (page marketing, liste de services publics). On pose des en-têtes sur la Response, et un proxy/CDN en amont sert la réponse sans exécuter PHP.

public function publicServices(): Response { $response = $this->render('public/services.html.twig', [ /* ... */ ]); $response->setPublic(); // cacheable par un proxy partagé $response->setMaxAge(3600); // frais pendant 1h $response->setEtag(md5($content)); // validation : le client peut demander "a-t-il changé ?" return $response; }

Deux stratégies :

  • Expiration (max-age) : « valable N secondes » — simple, mais peut servir du périmé.
  • Validation (ETag/Last-Modified) : le client demande « a-t-il changé ? » → 304 Not Modified si non (réponse minuscule).

Symfony fournit un reverse proxy (HttpCache) intégré pour démarrer, mais en prod on utilise Varnish ou un CDN (Cloudflare, Fastly). L’ESI (Edge Side Includes) permet de cacher une page par fragments (le bloc « recommandations » a un TTL différent du reste).

⚠️ Piège : mettre setPublic() sur une réponse personnalisée (contenant des données de l’utilisateur connecté) → un proxy partagé pourrait servir les données d’un utilisateur à un autre (fuite grave). Le cache public est réservé au contenu identique pour tous. Pour du contenu par utilisateur, setPrivate() (cache navigateur seulement) ou pas de cache. Ne cachez jamais publiquement une réponse authentifiée.

3. Le composant Cache (application)

Pour mémoriser un calcul coûteux ou une requête lente (ou un appel externe, Partie 13.3), on utilise CacheInterface :

use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; final class StatsService { public function __construct(private readonly CacheInterface $cache) {} public function dashboardStats(): array { return $this->cache->get('dashboard_stats', function (ItemInterface $item) { $item->expiresAfter(300); // 5 min return $this->computeExpensiveStats(); // exécuté seulement si absent du cache }); } }

get() renvoie la valeur cachée si présente, sinon exécute le callback, stocke le résultat et le renvoie. Le callback n’est exécuté que sur un cache miss.

4. Invalider avec des tags

Le vrai défi du cache : l’invalider au bon moment. Les tags (via TagAwareCacheInterface) permettent d’invalider par groupe :

return $this->cache->get('provider_' . $id, function (ItemInterface $item) use ($id) { $item->expiresAfter(3600); $item->tag(['providers', 'provider_' . $id]); // taggue l'entrée return $this->providerRepository->findWithServices($id); }); // plus tard, quand un prestataire change : $this->cache->invalidateTags(['provider_' . $id]); // invalide toutes les entrées taguées

On invalide typiquement dans un listener (Partie 5.5) sur le changement de l’entité, ou après un flush(). Combiné à un TTL (filet de sécurité), c’est la stratégie robuste : tags pour l’invalidation précise, TTL au cas où on oublie.

5. Redis : cache et sessions en production

En dev, le cache va sur le filesystem. En production multi-serveurs, on utilise Redis (partagé entre serveurs) :

# config/packages/cache.yaml framework: cache: app: cache.adapter.redis default_redis_provider: '%env(REDIS_URL)%'

Redis sert aussi de stockage de sessions partagé (rappel Partie 4.5 : sans stockage partagé, un utilisateur « saute » de session entre serveurs) :

framework: session: handler_id: '%env(REDIS_URL)%'

💡 Pour un dev Next.js : le cache HTTP ≈ le cache CDN et revalidate de Next (Partie 11.8) — même logique d’expiration/validation, appliquée par le proxy. Le composant Cache ≈ un unstable_cache ou un Redis côté serveur pour mémoriser des calculs. Et l’invalidation par tagsrevalidateTag de Next — la même idée des deux côtés ! Vous invalidez côté API (tags Symfony) et côté front (revalidateTag Next) pour une cohérence de bout en bout.

✏️ Exercices

Exercice 1. Mémorisez le résultat d’un calcul de statistiques pendant 10 minutes avec le composant Cache.

✅ Solution

return $this->cache->get('stats_dashboard', function (ItemInterface $item) { $item->expiresAfter(600); // 10 min return $this->computeStats(); });

Le callback n’est exécuté que sur un cache miss ; ensuite, la valeur est servie depuis le cache pendant 10 min.

Exercice 2. Pourquoi ne jamais mettre setPublic() sur une réponse contenant des données de l’utilisateur connecté ?

✅ Solution

Parce qu’un cache public (proxy/CDN partagé) stocke une version de la réponse et la sert à tous. Si elle contient des données personnelles, un autre utilisateur recevrait les données d’autrui — une fuite de données grave. Le cache public est réservé au contenu identique pour tous ; pour du contenu personnalisé, utiliser setPrivate() (cache navigateur uniquement) ou ne pas cacher.

Exercice 3. Comment invalider précisément le cache d’un prestataire quand il est modifié, sans vider tout le cache ?

✅ Solution

Avec les tags : à la mise en cache, tagger l’entrée ($item->tag(['provider_' . $id])) ; à la modification du prestataire, appeler $this->cache->invalidateTags(['provider_' . $id]) (typiquement dans un listener sur le changement de l’entité). Cela invalide uniquement les entrées de ce prestataire, sans toucher le reste du cache. On combine avec un TTL comme filet.

🧠 Quiz de révision

1. Quel niveau de cache est le plus rentable, et pourquoi ?

Le cache HTTP (proxy/CDN) : une réponse cachée est servie sans exécuter PHP ni toucher la base — coût serveur nul. Réservé au contenu identique pour tous.

2. Expiration vs validation en cache HTTP ?

Expiration (max-age) : valable N secondes. Validation (ETag/Last-Modified) : le client demande « a-t-il changé ? » → 304 si non (réponse minuscule).

3. Comment mémorise-t-on un calcul coûteux ?

Avec CacheInterface::get($key, $callback) : le callback n’est exécuté que sur un cache miss, le résultat est stocké (TTL via expiresAfter).

4. Comment invalide-t-on précisément ?

Avec des tags ($item->tag([...]) + invalidateTags([...])) — invalidation par groupe, plus un TTL en filet.

5. Pourquoi Redis en production ?

Pour un cache et des sessions partagés entre serveurs (sans quoi un utilisateur perd sa session/le cache en changeant de serveur).


Prochain chapitre : 15.2 — Profiler & performance — mesurer avant d’optimiser.