Aller au contenu principal

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.

info

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.

astuce

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.
remarque

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');
}
attention

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;
}
}
info

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


Quel est l'intérêt principal du cache applicatif ?


Quel service Symfony permet d'utiliser le cache applicatif ?


Quelle méthode de CacheInterface permet de récupérer une valeur en cache, ou de la calculer si elle est absente ?


Quelle méthode de ItemInterface définit la durée de vie d'un élément en cache ?


Que se passe-t-il si la clé demandée existe déjà en cache et n'a pas expiré ?


Quelle méthode de l'objet Response autorise des caches partagés (CDN, proxy) à conserver la réponse ?


Quelle méthode supprime manuellement une clé du cache ?


Quel risque court-on si l'on oublie d'invalider le cache après une modification des données ?


En production, quel type de cache est souvent préféré au cache fichier par défaut ?



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).


Bonne pratique - Choisir une durée de vie adaptée à la fraîcheur nécessaire

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.


Bonne pratique - En production, un cache plus performant que le cache fichier

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.

📌 Une solution