Le cache dans Symfony
Comment éviter de refaire les mêmes calculs ou les mêmes requêtes coûteuses à chaque requête HTTP
Notions théoriques
Pourquoi mettre en cache
Certaines opérations sont coûteuses : une requête Doctrine complexe sur une grosse table, un calcul statistique, un appel à une API externe... Si cette opération produit toujours le même résultat pendant un certain temps, il est inutile de la refaire à chaque requête HTTP.
Le cache consiste à conserver le résultat d'un calcul déjà effectué, pour le réutiliser directement lors des appels suivants, tant qu'il reste valide.
Mettre en cache n'est pas gratuit : cela ajoute de la complexité (il faut penser à l'invalider au bon moment) et consomme de l'espace de stockage. On réserve le cache aux opérations réellement coûteuses, pas à n'importe quel calcul trivial.
Le cache applicatif avec le composant Cache
Symfony fournit un composant Cache, accessible via le service CacheInterface. Son usage
repose sur une seule méthode principale : get(), qui prend une clé et un callback.
- Si la clé existe déjà en cache et n'a pas expiré, le callback n'est pas exécuté : la valeur stockée est directement retournée.
- Si la clé est absente (ou expirée), le callback est exécuté, et son résultat est automatiquement stocké en cache avant d'être retourn é.
public function getArticlesPopulaires(CacheInterface $cache): array
{
return $cache->get('articles_populaires', function (ItemInterface $item) {
$item->expiresAfter(300); // 5 minutes
// Ce code ne s'exécute que si le cache est vide ou expiré
return $this->articleRepository->findPopulaires();
});
}
L'objet ItemInterface, reçu en paramètre du callback, permet de configurer la durée de vie
de l'élément grâce à la méthode expiresAfter(), exprimée en secondes.
Choisissez une clé de cache explicite et unique ('articles_populaires'), pour éviter qu'elle
n'entre en collision avec une autre donnée mise en cache ailleurs dans l'application.
Mettre en cache une requête Doctrine coûteuse
Le cas d'usage le plus courant en début de carrière : éviter de recalculer une liste d'articles triée ou filtrée à chaque affichage de page, alors que le contenu du blog ne change que rarement.
// src/Service/ArticleManager.php
namespace App\Service;
use App\Repository\ArticleRepository;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class ArticleManager
{
public function __construct(
private ArticleRepository $articleRepository,
private CacheInterface $cache
) {}
public function getDerniersArticles(): array
{
return $this->cache->get('derniers_articles', function (ItemInterface $item) {
$item->expiresAfter(600); // 10 minutes
return $this->articleRepository->findBy([], ['datePublication' => 'DESC'], 10);
});
}
}
Le cache HTTP
Le composant Cache met en cache un résultat côté serveur. Il existe une autre approche, complémentaire : le cache HTTP, qui indique au navigateur ou à un serveur intermédiaire (reverse proxy, CDN) de conserver une réponse entière, sans même recontacter le serveur.
Cela se pilote via les en-têtes Cache-Control de la réponse HTTP. Symfony propose des méthodes
dédiées sur l'objet Response :
use Symfony\Component\HttpFoundation\Response;
public function index(): Response
{
$response = $this->render('article/liste.html.twig', [
'articles' => $this->articleManager->getDerniersArticles(),
]);
$response->setPublic();
$response->setSharedMaxAge(600); // 10 minutes, pour les caches partagés (CDN, proxy)
return $response;
}
setPublic(): autorise les caches partagés (pas seulement le navigateur de l'utilisateur) à conserver la réponse.setSharedMaxAge(): définit la durée de vie de la réponse en cache, en secondes.
Le cache applicatif (CacheInterface) et le cache HTTP répondent à des besoins différents :
le premier évite de refaire un calcul, le second évite de retraiter toute la requête.
Les deux peuvent tout à fait se combiner sur une même page.
Invalider le cache
Une donnée mise en cache doit un jour disparaître, sinon elle finit par afficher des informations obsolètes. Deux stratégies principales :
- L'expiration automatique : la donnée disparaît d'elle-même après le délai fixé par
expiresAfter(). - La suppression manuelle : on retire explicitement une clé du cache avec la méthode
delete(), typiquement juste après avoir modifié la donnée correspondante.
public function publierArticle(Article $article): void
{
$this->entityManager->persist($article);
$this->entityManager->flush();
// La liste des derniers articles vient de changer : on invalide le cache
$this->cache->delete('derniers_articles');
}
Oublier d'invalider le cache après une modification est une erreur fréquente : l'application semble
fonctionner (aucune erreur), mais affiche des données périmées jusqu'à l'expiration automatique.
Prenez l'habitude d'appeler delete() juste après chaque écriture qui rend une clé de cache obsolète.
Exemple de mise en application
// src/Controller/ArticleController.php
namespace App\Controller;
use App\Service\ArticleManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ArticleController extends AbstractController
{
#[Route('/articles', name: 'articles_liste')]
public function liste(ArticleManager $articleManager): Response
{
$response = $this->render('article/liste.html.twig', [
'articles' => $articleManager->getDerniersArticles(),
]);
$response->setPublic();
$response->setSharedMaxAge(600);
return $response;
}
}
Ici, deux niveaux de cache travaillent ensemble : le service ArticleManager évite de refaire la
requête Doctrine coûteuse pendant 10 minutes, et la réponse HTTP elle-même peut être conservée par
un CDN pendant la même durée, évitant même de recontacter le serveur Symfony.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Votre défi pour aujourd'hui consiste à mettre en cache le calcul du nombre total de commentaires du blog, une opération jugée coûteuse sur une grosse table, puis à invalider ce cache lors de l'ajout d'un nouveau commentaire.
Étape 1 — Injecter le service de cache
Le service StatistiquesManager doit recevoir le service de cache par injection de dépendance.
Étape 2 — Mettre en cache le comptage des commentaires
La méthode compterCommentaires() doit utiliser le cache, avec une durée de vie de 5 minutes (300 secondes).
5 minutes de cache pour un compteur de commentaires est un bon compromis : la valeur affichée reste raisonnablement à jour, tout en évitant de recompter à chaque affichage de page. Pour une donnée qui change rarement (catégories d'un blog, par exemple), une durée de vie beaucoup plus longue serait justifiée.
Étape 3 — Invalider le cache après l'ajout d'un commentaire
Quand un commentaire est ajouté, le compteur mis en cache devient obsolète : il faut le supprimer.
Par défaut, Symfony stocke le cache applicatif dans des fichiers, ce qui convient très bien pour
apprendre et développer en local. En production, sur un serveur qui reçoit beaucoup de trafic,
on préfère généralement un cache en mémoire comme Redis ou APCu, nettement plus rapide
à lire et à écrire qu'un accès au système de fichiers. C'est un réglage de configuration, sans
aucun changement dans le code des services qui utilisent CacheInterface.