← Tous les cours / Symfony Cache avancé Cours
00 — PSR-6, PSR-16, CACHEINTERFACE

PSR-6, PSR-16 et l'interface Symfony

Symfony Cache implémente trois interfaces — PSR-6 (CacheItemPool), PSR-16 (SimpleCache), et sa propre CacheInterface avec callback.

Le composant Cache de Symfony implémente trois contrats distincts. Comprendre lequel utiliser — et pourquoi — est la première étape vers un cache sans surprise.

PSR-6 — CacheItemPoolInterface

Défini par PHP-FIG, PSR-6 est l'interface bas niveau. Tu travailles explicitement avec des CacheItem : tu les récupères, tu les modifies, tu les sauvegardes.

// PSR-6 : récupérer → vérifier → utiliser ou calculer → sauvegarder
$item = $pool->getItem('product_42');

if (!$item->isHit()) {
    $data = $this->repository->find(42);
    $item->set($data);
    $item->expiresAfter(3600);
    $pool->save($item);
}

return $item->get();
CacheItemPoolInterface — méthodes principales
getItem(string $key): CacheItemInterface
getItems(array $keys): iterable
hasItem(string $key): bool
save(CacheItemInterface $item): bool
saveDeferred(CacheItemInterface $item): bool
commit(): bool
deleteItem(string $key): bool
clear(): bool

PSR-16 — SimpleCache

Plus simple, sans objet CacheItem intermédiaire. API get/set/delete/clear directement sur les valeurs. Adapté aux cas simples, sans TTL par item ni tags.

// PSR-16 : API directe sur les valeurs
$data = $cache->get('product_42', null);

if ($data === null) {
    $data = $this->repository->find(42);
    $cache->set('product_42', $data, 3600);
}

return $data;

CacheInterface Symfony — le pattern callback

L'interface propre à Symfony introduit le pattern callback : tu passes une fonction qui calcule la valeur si elle n'est pas en cache. Symfony gère le hit/miss, le TTL, et la protection contre le stampede.

use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

$data = $cache->get('product_42', function (ItemInterface $item) {
    $item->expiresAfter(3600);
    return $this->repository->find(42); // calculé seulement si miss
});
Signature complète de CacheInterface::get()
get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed

Le callback reçoit CacheItemInterface $item et bool &$save (optionnel). Mettre $save = false dans le callback empêche la mise en cache du résultat.
🔑 Pourquoi le callback est supérieur à get+set ? Parce qu'il est atomique du point de vue du cache : Symfony peut verrouiller la clé pendant le calcul pour éviter que deux workers calculent la même valeur simultanément. De plus, le TTL est défini dans le callback, au plus près de la logique de calcul.

RESERVED_CHARACTERS

Les caractères {}()/\@: sont interdits dans les clés et dans les noms de tags. Tout adapter les rejette avec une InvalidArgumentException à la construction. Utilise des tirets, des underscores et des points à la place.

Clique sur une interface pour voir le pattern de code correspondant.

01 — LES ADAPTERS

Les adapters

Un adapter est le pilote derrière le pool — il définit où les données sont stockées.

Chaque adapter implémente AdapterInterface (qui étend CacheItemPoolInterface de PSR-6) et expose les mêmes méthodes. Choisir le bon adapter, c'est choisir le bon stockage selon le contexte : développement, production mono-serveur, production distribuée, tests.

Tableau des adapters

Adapter Stockage Contexte recommandé
FilesystemAdapter Fichiers disque Dev, prod mono-serveur sans Redis
ArrayAdapter Tableau PHP en mémoire Tests, cache intra-requête
RedisAdapter Redis Prod multi-serveur, tags, haute performance
MemcachedAdapter Memcached Prod multi-serveur (sans tags natifs)
PdoAdapter Base de données SQL Quand seule la BDD est disponible
ApcuAdapter APCu (mémoire partagée PHP) Prod mono-serveur, très faible latence
ChainAdapter Plusieurs adapters en cascade Multi-niveaux : APCu + Redis
NullAdapter Rien (blackhole) Tests — désactive complètement le cache
ProxyAdapter Délègue à un PSR-6 Adapter une bibliothèque PSR-6 tierce

Constructeurs principaux

FilesystemAdapter

new FilesystemAdapter(
    $namespace  = '',        // préfixe de clé
    $defaultLifetime = 0,  // 0 = pas d'expiration par défaut
    $directory  = null,     // null = sys_get_temp_dir()/symfony-cache
    $marshaller = null,
)

ArrayAdapter

new ArrayAdapter(
    $defaultLifetime  = 0,
    $storeSerialized  = true,  // false = stocke la référence PHP directement
    $maxLifetime      = 0,    // TTL maximum (plafond)
    $maxItems         = 0,    // 0 = illimité. Si > 0 : LRU (éviction du plus ancien)
)
ArrayAdapter et maxItems
Quand $maxItems > 0, l'adapter applique une politique LRU (Least Recently Used) : le plus ancien item non utilisé est évincé quand la limite est atteinte. Utile pour un cache intra-requête de taille bornée.

ApcuAdapter

new ApcuAdapter(
    $namespace       = '',
    $defaultLifetime = 0,
    $version         = null,  // changer $version invalide tout le namespace
    $marshaller      = null,
)
ApcuAdapter et $version
APCu ne supporte pas clear() par préfixe. Le paramètre $version est intégré dans les clés stockées — changer la version crée de nouvelles clés, les anciennes expirent naturellement. C'est l'équivalent d'un namespace versionné.

PdoAdapter

new PdoAdapter(
    $connOrDsn,        // PDO|Connection|string DSN
    $namespace       = '',
    $defaultLifetime = 0,
    $options         = [],  // db_table, db_id_col, db_data_col, db_lifetime_col, db_time_col
    $marshaller      = null,
)

ProxyAdapter

// Envelopper un pool PSR-6 tiers pour lui ajouter CacheInterface
new ProxyAdapter($psr6Pool, $namespace = '', $defaultLifetime = 0)
💡 Guide de choix rapide : Dev → FilesystemAdapter. Tests → ArrayAdapter ou NullAdapter. Prod mono-serveur → ApcuAdapter ou FilesystemAdapter. Prod multi-serveur → RedisAdapter. Besoin de tags → TagAwareAdapter(new RedisAdapter(...)). Performance maximale → ChainAdapter([new ApcuAdapter(), new RedisAdapter()]).
02 — CACHEITEM

CacheItem — TTL, tags et métadonnées

CacheItem est plus qu'une paire clé-valeur — il porte sa date d'expiration, ses tags et ses métadonnées de création.

Un CacheItem est l'objet que tu manipules dans le pattern PSR-6 ou dans le callback de CacheInterface::get(). Il stocke la valeur, son expiration, ses tags et ses métadonnées.

Méthodes de base

$item->getKey();         // string — clé du cache
$item->get();            // mixed  — valeur stockée (null si miss)
$item->isHit();          // bool   — true si l'item existe et n'est pas expiré
$item->set($value);     // static — définir la valeur

TTL — deux approches

expiresAt() — date absolue

$item->expiresAt(new \DateTime('+1 hour'));
$item->expiresAt(new \DateTimeImmutable('2026-12-31 23:59:59'));
$item->expiresAt(null);  // expiration indéfinie (durée par défaut du pool)

expiresAfter() — durée relative

$item->expiresAfter(3600);                            // en secondes
$item->expiresAfter(new \DateInterval('PT1H'));      // intervalle
$item->expiresAfter(null);                            // durée par défaut du pool
expiresAt vs expiresAfter
expiresAt() reçoit un \DateTimeInterface — la date d'expiration est absolue. expiresAfter() reçoit un int (secondes) ou un \DateInterval — la durée est calculée à partir du moment de l'appel. En pratique, expiresAfter() est plus lisible pour les durées courantes.

Tags

// Ajouter un ou plusieurs tags (nécessite TagAwareAdapter)
$item->tag('user_42');
$item->tag(['user_42', 'profile', 'global']);

// Dans le callback CacheInterface :
$cache->get('user_42_profile', function (ItemInterface $item) {
    $item->expiresAfter(3600);
    $item->tag(['user_42', 'profile']);
    return $this->userRepository->findProfile(42);
});
⚠️ Piège : appeler tag() sur un item obtenu depuis un adapter non-tagaware lève une LogicException à la sauvegarde. Assure-toi que ton pool est un TagAwareAdapter ou qu'il implémente TagAwareCacheInterface.

Métadonnées

$metadata = $item->getMetadata();

// Clés disponibles via les constantes ItemInterface :
$metadata[ItemInterface::METADATA_EXPIRY];  // float — timestamp d'expiration
$metadata[ItemInterface::METADATA_CTIME];   // int — timestamp de création (ms)
$metadata[ItemInterface::METADATA_TAGS];    // array — tags de l'item

// Ou via le 4e argument de CacheInterface::get() :
$cache->get('ma_cle', fn($item) => 42, null, $metadata);
// $metadata est maintenant rempli après l'appel
RESERVED_CHARACTERS = {}()/\@:
Interdits dans les clés et dans les noms de tags. Toute clé contenant un de ces caractères lève une InvalidArgumentException. Utilise des tirets, underscores et points : user_42.profile, product-catalog-fr.
03 — CHAINADAPTER

ChainAdapter — cache multi-niveaux

Le ChainAdapter empile plusieurs adapters — lecture du plus rapide au plus lent, propagation vers le haut.

Imagine un bibliothécaire avec deux niveaux de rangement : un présentoir en surface pour les livres très demandés, et des rayonnages en sous-sol pour les autres. Si le livre est en surface, tu l'as immédiatement. Sinon, il descend chercher — et au retour, il en met un exemplaire en surface pour la prochaine fois. C'est exactement le ChainAdapter.

Constructeur

use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;

$cache = new ChainAdapter([
    new ApcuAdapter('', 300),      // niveau 0 : mémoire partagée PHP, 5 min
    new RedisAdapter($redis, '', 3600), // niveau 1 : Redis, 1 h
], 300); // $defaultLifetime = durée de propagation vers niveaux supérieurs
Algorithme de lecture
1. Cherche dans adapters[0] (APCu). Hit → retourne la valeur.
2. Si miss → cherche dans adapters[1] (Redis). Hit → propage dans APCu avec $defaultLifetime → retourne la valeur.
3. Si miss partout → le callback est appelé → sauvegarde dans tous les adapters.

Write-back et defaultLifetime

Quand un item est trouvé dans un adapter de niveau N, il est propagé dans tous les adapters 0 à N-1 avec le $defaultLifetime du ChainAdapter. Ce n'est pas la durée de l'item original — c'est la durée de la copie dans les niveaux supérieurs.

$defaultLifetime de ChainAdapter
Ne confonds pas ce paramètre avec le TTL de l'item dans le backend final (Redis). Il contrôle uniquement combien de temps la copie remontée dans APCu est conservée. Mettre 0 ici signifie que les copies propagées n'expirent jamais dans les niveaux supérieurs — ce qui peut créer des données périmées si Redis expulse l'item mais qu'APCu garde sa copie.

Avantages et limites

Avantages
  • Latence réduite sur les items chauds (APCu ≈ 1 µs vs Redis ≈ 1 ms)
  • Résilience : si Redis est indisponible, les items en APCu continuent de servir
  • Transparent pour le code consommateur
Limites
  • Données potentiellement désynchronisées entre niveaux pendant $defaultLifetime
  • L'invalidation sur un seul niveau ne propage pas aux autres
  • APCu est local à chaque worker PHP — cohérence inter-serveurs impossible

ChainAdapter en YAML

# config/packages/cache.yaml
framework:
    cache:
        pools:
            cache.system:
                adapter: cache.adapter.chain
                adapters:
                    - cache.adapter.apcu
                    - cache.adapter.redis
                default_lifetime: 300
🔑 À retenir : le ChainAdapter ne supporte pas les tags nativement. Pour un cache multi-niveaux avec tags, enveloppe chaque adapter dans un TagAwareAdapter avant de les passer au ChainAdapter.
04 — TAGS ET INVALIDATION

Tags et invalidation

Les tags permettent d'invalider un groupe d'items en une seule opération — sans connaître leurs clés.

Imagine que tu as mis en cache le profil de l'utilisateur 42 sous des dizaines de clés différentes : photo de profil, préférences, panier, historique… Quand il change son email, tu dois invalider tout ça. Avec les clés, c'est impossible à tenir. Avec les tags, c'est une ligne.

TagAwareCacheInterface

use Symfony\Contracts\Cache\TagAwareCacheInterface;

// Invalider tous les items taggés 'user_42'
$cache->invalidateTags(['user_42']); // bool

// Invalider plusieurs tags d'un coup
$cache->invalidateTags(['user_42', 'products']);

TagAwareAdapter

use Symfony\Component\Cache\Adapter\TagAwareAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;

// itemsPool + tagsPool séparé (recommandé en prod)
$cache = new TagAwareAdapter(
    new RedisAdapter($redis),            // pool des items
    new RedisAdapter($redis, 'tags'),   // pool des versions de tags
    0.15                                 // knownTagVersionsTtl en secondes
);

// Écriture avec tags :
$cache->get('user_42_profile', function (ItemInterface $item) {
    $item->expiresAfter(3600);
    $item->tag(['user_42', 'profile']);
    return $this->userRepository->findProfile(42);
});

Fonctionnement interne

Chaque tag a une clé de version stockée dans le $tagsPool. Quand un item est écrit, ses versions de tags sont stockées avec lui. À la lecture, Symfony compare les versions stockées dans l'item avec les versions actuelles des tags. Si une version ne correspond plus, l'item est considéré comme invalide (miss).

invalidateTags() en détail
Symfony supprime les clés de version dans le $tagsPool. Lors de la prochaine lecture d'un item taggé, la comparaison de versions échoue → l'item est recomputé et les nouvelles versions sont stockées. L'item "invalide" n'est pas supprimé physiquement — il est ignoré jusqu'à ce qu'il soit remplacé ou qu'il expire.
knownTagVersionsTtl (0.15s par défaut)
Pour éviter un aller-retour Redis à chaque appel de get(), Symfony met en cache les versions de tags en mémoire locale pendant knownTagVersionsTtl secondes. Si tu invalides un tag, les workers qui ont une version en cache local peuvent servir l'ancien item pendant au maximum 0.15 secondes. C'est un compromis performance/cohérence acceptable dans la plupart des cas.
💡 $tagsPool séparé : utilise un pool Redis dédié pour les tags. Ça permet de configurer une politique de persistance différente (les versions de tags sont petites et doivent survivre aux redémarrages, contrairement aux valeurs de cache).

Lance la simulation ci-dessous pour voir l'invalidation par tags en action.

Tape un tag dans le champ et clique sur invalidateTags.

05 — EARLY EXPIRATION

Early expiration et beta

L'early expiration probabiliste recalcule le cache avant qu'il expire — c'est la protection contre le "cache stampede".

Imagine une page très populaire dont le cache expire à minuit pile. À minuit, 500 workers arrivent simultanément, voient tous un miss, et lancent tous le même calcul lourd en parallèle. C'est le cache stampede. L'early expiration probabiliste évite ça.

L'algorithme XFetch

Plutôt que d'attendre l'expiration réelle, chaque worker calcule probabilistiquement s'il doit regénérer le cache avant qu'il expire. La probabilité augmente exponentiellement à mesure qu'on approche de l'expiration.

Formule simplifiée
now - delta * beta * log(rand(0,1)) ≥ expiry

Si cette condition est vraie, le worker regénère. delta est le temps de calcul estimé. beta contrôle l'agressivité de la recomputation précoce.

Le paramètre beta

// beta = 0 : jamais de recomputation précoce (stampede possible)
$cache->get('expensive_data', fn($item) => compute(), 0);

// beta = 1.0 (défaut) : protection optimale pour la plupart des cas
$cache->get('expensive_data', fn($item) => compute());

// beta = INF : force la recomputation immédiate (tests uniquement)
$cache->get('expensive_data', fn($item) => compute(), INF);
betaComportementCas d'usage
0 Pas de recomputation précoce. L'item expire à sa date exacte. Données peu coûteuses à recalculer
1.0 (défaut) Protection optimale. Un seul worker recompute avant expiration. La grande majorité des cas
INF Force la recomputation à chaque accès. Ignore le cache. Tests, débogage

Forcer un miss avec $save = false

Dans le callback, le second paramètre optionnel bool &$save permet de calculer une valeur sans la mettre en cache :

$data = $cache->get('api_result', function (ItemInterface $item, bool &$save) {
    try {
        $result = $this->apiClient->fetch();
        $item->expiresAfter(300);
        return $result;
    } catch (\Exception $e) {
        $save = false; // erreur réseau : ne pas cacher le résultat
        return $this->fallback();
    }
});
🔑 À retenir : l'early expiration ne fonctionne que si l'item a une expiration définie (expiresAfter ou expiresAt). Sur un item sans TTL, beta est ignoré. Pour des données populaires et coûteuses à calculer, c'est une protection passive qui ne demande aucune infrastructure supplémentaire — il suffit de ne pas passer beta=0.
06 — DEFERRED SAVE

saveDeferred() et commit()

saveDeferred() diffère la sauvegarde pour les opérations batch — commit() les envoie d'un coup.

Dans une boucle qui met en cache 500 items, faire un aller-retour Redis par item = 500 round-trips. saveDeferred() accumule les items à sauvegarder, et commit() les envoie en un seul pipeline. De 500 requêtes réseau à 1 — voilà l'intérêt.

Pattern deferred

// PSR-6 — saveDeferred + commit
foreach ($products as $product) {
    $item = $pool->getItem('product_' . $product->getId());
    if (!$item->isHit()) {
        $item->set($product->toArray());
        $item->expiresAfter(3600);
        $pool->saveDeferred($item); // accumule, ne sauvegarde pas encore
    }
}

$pool->commit(); // envoie tout en un seul pipeline
saveDeferred() vs save()
save(CacheItemInterface $item): bool — sauvegarde immédiate, un aller-retour par item.
saveDeferred(CacheItemInterface $item): bool — accumule en mémoire sans écriture réseau.
commit(): bool — envoie tous les items deferred en une seule opération batch.

commit() automatique dans __destruct()

Les adapters Symfony appellent commit() dans leur destructeur. Si ton script se termine normalement, les items deferred sont sauvegardés même si tu n'appelles pas commit() explicitement. C'est une protection, pas une invitation à l'oubli.

⚠️ Risque : si une exception est levée entre saveDeferred() et commit(), et que l'exception remonte jusqu'au destructeur avant l'appel, les items peuvent être perdus silencieusement selon le contexte d'exécution (FPM vs CLI vs long-running process). Appelle toujours commit() explicitement dans les opérations critiques.

Cas Redis — pipeline

Pour Redis, commit() utilise le pipelining : les commandes SET sont empilées localement et envoyées en un seul paquet TCP. La latence d'un SET x 500 avec pipeline est comparable à celle d'un seul SET.

// Équivalent Redis interne :
// PIPELINE
// SET product_1 "..." EX 3600
// SET product_2 "..." EX 3600
// ... (500 SET)
// EXEC
// → 1 seul aller-retour réseau
🔑 À retenir : saveDeferred n'est disponible que dans l'interface PSR-6 (CacheItemPoolInterface). L'interface Symfony CacheInterface utilise le pattern callback et gère l'optimisation en interne — tu n'as pas accès à saveDeferred via elle.
07 — CONFIGURATION

Configuration Symfony (cache.yaml)

La configuration Symfony crée des pools nommés injectables — chaque pool est un service.

Plutôt que d'instancier les adapters à la main, Symfony crée des pools nommés à partir de la configuration YAML. Chaque pool est un service injectable, tagué cache.pool.

Configuration de base

# config/packages/cache.yaml
framework:
    cache:
        default_redis_provider: '%env(REDIS_DSN)%'

        pools:
            cache.app:
                adapter: cache.adapter.filesystem

            cache.redis:
                adapter: cache.adapter.redis
                provider: '%env(REDIS_DSN)%'
                default_lifetime: 3600

            cache.tagged:
                adapter: cache.adapter.redis
                tags: true  # → TagAwareAdapter automatique

            cache.multi:
                adapter: cache.adapter.chain
                adapters:
                    - cache.adapter.apcu
                    - cache.adapter.redis
                default_lifetime: 300

DSN Redis

Format DSNDescription
redis://localhostRedis local, port 6379 par défaut
redis://user:pass@host:6380Avec authentification
rediss://host:6380TLS (redis over SSL)
redis:?host[h1]&host[h2]Cluster Redis

Injection dans un service

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;

public function __construct(
    // Pool spécifique par ID de service
    #[Autowire(service: 'cache.redis')]
    private readonly CacheInterface $cache,

    // Pool tagaware
    #[Autowire(service: 'cache.tagged')]
    private readonly TagAwareCacheInterface $taggedCache,
) {}
tags: true
Quand un pool est déclaré avec tags: true, Symfony le configure automatiquement comme un TagAwareAdapter. Le service implémente alors TagAwareCacheInterface et peut être injecté avec ce type.
💡 Pool cache.system : Symfony expose un pool cache.system destiné aux données stables du framework (annotations, routes compilées…). Il n'est pas vidé par cache:clear — seulement par cache:pool:clear cache.system. Ne l'utilise pas pour des données applicatives changeantes.
🔑 Commandes de gestion :
php bin/console cache:pool:list — lister tous les pools déclarés
php bin/console cache:pool:clear cache.redis — vider un pool spécifique
php bin/console cache:pool:prune — supprimer les items expirés (FilesystemAdapter)
08 — MARSHALLER & NAMESPACE

Marshaller et namespace

Le marshaller contrôle la sérialisation des données en cache — le namespace isole les pools partageant le même backend.

Deux mécanismes orthogonaux contrôlent la façon dont les données sont stockées dans le backend : le marshaller sérialise (et éventuellement chiffre) les valeurs avant écriture, le namespace préfixe les clés pour isoler les pools partageant le même serveur.

MarshallerInterface

interface MarshallerInterface
{
    /** @param mixed[] $values  @param string[]|null &$failed */
    public function marshall(array $values, ?array &$failed): array;

    public function unmarshall(string $value): mixed;
}

DefaultMarshaller

Utilisé par défaut. Il sérialise avec serialize() / unserialize(). Si l'extension igbinary est disponible, elle est utilisée automatiquement à la place — plus compact et plus rapide.

use Symfony\Component\Cache\Marshaller\DefaultMarshaller;

new RedisAdapter(
    $redis,
    $namespace  = '',
    $lifetime   = 0,
    $marshaller = new DefaultMarshaller(useIgbinary: true), // force igbinary
)

SodiumMarshaller — données chiffrées au repos

use Symfony\Component\Cache\Marshaller\SodiumMarshaller;

$marshaller = new SodiumMarshaller(
    [sodium_crypto_box_keypair()], // tableau de clés (rotation possible)
    new DefaultMarshaller(),
);

new RedisAdapter($redis, '', 0, $marshaller);
SodiumMarshaller
Chiffre les données avec libsodium avant de les écrire dans Redis. Utile quand le backend est partagé entre plusieurs applications ou accessible à des tiers. Le tableau de clés permet la rotation : la première clé sert à chiffrer, toutes les clés servent à déchiffrer.

Namespace — isoler les pools

Le $namespace est un préfixe inséré dans chaque clé avant l'écriture dans le backend. Deux pools Redis avec des namespaces différents peuvent coexister sur le même serveur sans collision.

// Pool "orders" et pool "catalog" sur le même Redis
new RedisAdapter($redis, 'orders', 3600);
new RedisAdapter($redis, 'catalog', 7200);

// La clé Redis réelle sera du type :
// orders:Ab3xK9...:product_42
// catalog:Zf7mQ2...:product_42

clear() avec namespace

$cache->clear(); // vide UNIQUEMENT les clés du namespace de ce pool
                 // les autres namespaces sur le même Redis ne sont pas affectés
ApcuAdapter et $version
APCu ne supporte pas clear() par préfixe. Le paramètre $version est intégré dans les clés : changer la version crée de nouvelles clés (les anciennes expirent naturellement). C'est l'invalidation de namespace sans suppression physique.
🔑 À retenir : le namespace est aussi utilisé par Symfony pour isoler les pools déclarés en YAML sur le même backend. Deux pools cache.redis et cache.tagged pointant sur le même Redis auront des namespaces auto-générés différents — pas de collision même si tu n'en configures pas.
09 — SYNTHÈSE

Synthèse — le guide de choix

Le guide de choix des adapters et les anti-patterns du cache.

Le composant Cache de Symfony est riche mais cohérent. Si tu sais quel problème tu résous, le bon outil est évident. Voici le guide de choix.

Guide de choix des adapters

ContexteAdapter recommandéRaison
Développement local FilesystemAdapter Pas de dépendance externe, persistant entre requêtes
Tests unitaires ArrayAdapter ou NullAdapter Array = comportement réaliste, Null = désactive le cache
Prod mono-serveur (PHP process partagés) ApcuAdapter Mémoire partagée entre workers FPM, latence microseconde
Prod multi-serveur RedisAdapter Cache centralisé, cohérence inter-serveurs
Prod multi-serveur + tags TagAwareAdapter(RedisAdapter) Invalidation par tags requiert Redis
Performance maximale ChainAdapter([ApcuAdapter, RedisAdapter]) APCu pour les hits chauds, Redis pour la cohérence
Données sensibles au repos RedisAdapter + SodiumMarshaller Chiffrement avant écriture dans Redis
Seule la BDD est disponible PdoAdapter Stockage SQL, moins performant mais sans infra supplémentaire

Anti-patterns

🚫 Cache à durée indéfinie sans stratégie d'invalidation.
Si tu n'appelles jamais delete() ni invalidateTags(), les données périmées restent indéfiniment. Mets toujours un TTL réaliste — ou implémente une invalidation explicite à la mutation des données.
🚫 Clés non namespaceées en multi-tenant.
Stocker user_42 sans namespace quand plusieurs tenants partagent le même Redis crée des fuites de données entre tenants. Utilise toujours un namespace ou préfixe tenant dans la clé : tenant_A.user_42.
🚫 cache:clear en production sur un site à fort trafic.
Vider tout le cache d'un coup crée un stampede massif. Préfère l'invalidation par tags ou par pool spécifique (cache:pool:clear cache.catalog). En dernier recours, utilise un warm-up de cache après le clear.
🚫 beta = 0 sur des données lourdes populaires.
Désactiver l'early expiration sur une page calculée en 2 secondes avec 1000 req/s à l'expiration, c'est garantir 2000 requêtes simultanées vers la BDD. Garde beta = 1.0 par défaut.

Récapitulatif des interfaces

InterfaceMéthodes clésQuand l'utiliser
CacheItemPoolInterface (PSR-6) getItem / save / saveDeferred / commit Batch, contrôle fin sur les items, interopérabilité
CacheInterface (PSR-16) get(key, default) / set(key, val, ttl) API simple, pas de tags, cas basiques
CacheInterface (Symfony) get(key, callback, beta) / delete Pattern callback, stampede protection, tags
TagAwareCacheInterface hérite Symfony CacheInterface + invalidateTags Invalidation groupée sans connaître les clés