Web

Le cache pool de Symfony

by Stéphane Meaudre 3 mars 2021

Parmi tous ses différents composants, Symfony en propose un qui peut se révéler très pratique pour gérer et utiliser différents systèmes de cache : le cache pool. Pour rappel, le but du cache est de permettre l’accélération d’exécution de nos pages et ainsi améliorer grandement les performances de notre site ou de notre application. Le composant se base sur des classes Adapters, implémentant l’interface AdapterInterface, et pouvant être prêt à l’emploi pour interagir avec Redis, Memcached et plein d’autres systèmes de cache.

Quelques bases

L’installation du composant s’effectue via composer :

composer require symfony/cache

Son fonctionnement est basé sur deux implémentations des règles PSR (PHP Standards Recommendations) :

  • PSR 6 : règles permettant aux développeurs de développer leurs propres packages prenant en charge le cache (pour des frameworks, des paquets d’applications etc.).
  • PSR 16 : règles décrivant une interface simple et extensible pour manipuler des items en cache ainsi qu’un driver pour piloter l’accès au système de cache.

Dans le cas d’une utilisation de Redis ou Memcached, certaines librairies PHP supplémentaires sont à installer.

Exemple avec Memcached :

sudo apt-get install memcached
sudo apt-get install -y php7.x-memcached #php7.x correspond à la version de PHP voulu 

Utilisation par défaut

L’utilisation d’un Adapter est possible directement, sans forcément passer par un service. Symfony proposant plusieurs Adapters, chacun possédant diverses méthodes permettant de manipuler le cache. Certains sont destinés à des besoins bien précis.

Exemple avec le FileSystemAdapter :

use Symfony\Component\Cache\Adapter\FilesystemAdapter;
 
// Set CacheAdapter
$cachePool = new FilesystemAdapter('', 0, "cache");
 
// Check if item exist
if ($cachePool->hasItem('key_item')) {
    // Récupération
    $item = $cachePool->getItem('key_item');
    if ($item->isHit()) {
        echo $item->get();
    }
}
 
//Save item or create new one
$item = $cachePool->getItem('key_item');
if (!item->isHit()) {
    // Sauvegarde
    $item->set('new_value');
    $cachePool->save($item);
}
 
// Suppression d'un item et vérification si item existe ou non
$cachePool->deleteItem('key_item');
if (!$cachePool->hasItem('key_item')) {
    echo 'Item deleted';
}
 
// Suppression de tous les items du cache
$cachePool->clear();

Liste des Adapters

  • Memcached Cache Adapter : stocke des valeurs en mémoire dans une instance Memcached
  • Redis Cache Adapter : similaire au Memcached Cache Adapter, mais pour Redis
  • APCu Cache Adapter : cache dédié aux hautes performances à l’aide d’un cache partagé
  • Array Cache Adapter : généralement utilisé pour du test, le contenu étant stocké en mémoire
  • Chain Cache Adapter : combine les items des autres Adapters disponibles
  • Doctrine Cache Adapter : englobe toutes classes héritant du abstract provider du Cache Doctrine
  • PDO & Doctrine DBAL Cache Adapter : Adapter stockant les items dans une base de données SQL
  • Filesystem Cache Adapter : stocke les items dans des fichiers. Moins performant que APCu ou Memcached/Redis Adapter
  • PHP Array Cache Adapter : Adapter de haute performance pour données statiques (ex: configuration) et stocké dans OPcache
  • PHP Files Cache Adapter : similaire au FileSysteme Adapter mais stocke sous forme de code PHP
  • Proxy Cache Adapter

Développement

À partir de là, nous pouvons développer notre propre service dans lequel nous injectons l’Adapter souhaité ainsi qu’une interface contenant diverses méthodes permettant de manipuler très facilement notre Cache Adapter. Néanmoins, un peu de configuration est nécessaire avant de le coder. Nous allons illustrer ceci avec un cas d’utilisation concret : développons un service qui utilise l’Adapter de Memcached.

Dans le fichier config\packages\framework.yaml, il faut définir l’Adapter qui sera utilisé : 

framework:
  cache:
    prefix_seed: <mypool>
    default_memcached_provider: '%env(MEMCACHED_URL)%' # specific memcached
    pools:
      app.cache.mypool:
        adapter: cache.adapter.memcached
        public: false
        default_lifetime: 31536000  

Nous allons maintenant créer la classe dans laquelle nous injecterons app.cache.mypool (le terme “mypool” est à changer selon votre besoin) définie juste avant. Notre classe va implémenter une interface nommée CachePoolInterface dans laquelle nous allons déclarer la liste des différentes méthodes que nous voulons utiliser : ce qui est très utile si nous avons plusieurs services avec chacun un Adapter qui lui est propre.

Déclarons notre service dans le services.yaml :

 services:
    // ...
    # Cache pool
    App\Services\Cache\MemcachedService:
        arguments:
            $cachePool: '@app.cache.mypool'

L’ossature de notre service :

namespace App\Services\Cache;
 
use Symfony\Contracts\Cache\CacheInterface;
 
final class MemcachedService
{
    private CachePoolInterface $cachePool;
 
    /**
     * CachepoolService constructor.
     *
     * @param CachePoolInterface $cachePool
     */
    public function __construct(CachePoolInterface $cachePool)
    {
        $this->cachePool = $cachePool;
    }
}

Créons maintenant l’interface dans laquelle nous ajouterons 5 méthodes :

  • getItem() : récupérer un item en cache via la clé
  • saveItem() : sauvegarder un item
  • hasItem() : vérifier si un item existe
  • deleteItem() : supprimer un item
  • deleteAll() : supprimer tous les items
interface CachePoolInterface
{
   /**
    * @param $key
    *
    * @return string|null
    */
   public function getItem(string $key): ?string;

   /**
    * @param $key
    * @param $value
    *
    * @return bool
    */
   public function saveItem(string $key, $value): bool;

   /**
    * @param $key
    *
    * @return bool
    */
   public function hasItem(string $key): bool;
  
   /**
    * @param $key
    *
    * @return bool
    */
   public function deleteItem(string $key): bool;

   /**
    * @return bool
    */
   public function deleteAll(): bool;
}

Implémentons l’interface à notre service et générons les méthodes :

final class CachepoolService implements CachePoolInterface
{
    private CachePoolInterface $cachePool;

    /**
     * CachepoolService constructor.
     * @param CachePoolInterface $cachePool
     */
    public function __construct(CachePoolInterface $cachePool)
    {
        $this->cachePool = $cachePool;
    }

    /**
     * @param $key
     * @return bool|mixed
     */
    public function getItem(string $key): ?string
    {
        $cacheItem = $this->cachePool->getItem($key);
        return ($cacheItem->isHit()) ? $cacheItem->get(): null;
    }

    /**
     * @param $key
     * @param $value
     * @return bool
     */
    public function saveItem(string $key, $value): bool
    {
        $cacheItem = $this->cachePool->getItem($key);
        $cacheItem->set($value);
        return $this->cachePool->save($cacheItem);
    }

    /**
     * @param $key
     * @return bool
     */
    public function hasItem(string $key): bool
    {
        return $this->cachePool->hasItem($key);
    }

    /**
     * @param $key
     * @return bool
     */
    public function deleteItem(string $key): bool
    {
        return $this->cachePool->deleteItem($key);
    }

    /**
     * @return bool
     */
    public function deleteAll(): bool
    {
        return $this->cachePool->clear();
    }
}

Utilisation

Notre service étant maintenant opérationnel, nous pouvons donc l’utiliser. Dans l’exemple ci-dessous, nous souhaitons aller chercher un item dans le cache si ce dernier existe.

 

 /* 
 * @Route("/my-route/$id", name="my_route")
 *
 * @param string $id
 * @param CachePoolInterface $cachePool
 *
 * @return Response
 */
public function show(string $id, CachePoolInterface $cachePool): Response
{
    $keyItem = md5($id);
    if ($cachePool->hasItem($keyitem)) {
        $item = $cachePool->getItem($keyitem);
        // If we store Object, we need to deserialize it
        $itemEntity = unserialize($item);
    }
    //...
    $response = new Response();
    //... 
    return $this->render('template.html.twig', ['entity' => $itemEntity], $response);
}

Autre exemple pour sauvegarder les données :

$id = 1;
 
/** @var Foo $foo */
$foo = new Foo();
$foo->setId($id);
$foo->setBar('toto');
 
// Add data
$cachePool->saveItem(md5($id), serialize($foo));
// ...
 
// Retrieve class Foo stored in Cache Pool
$serializedFoo = $cachePool->getItem(md5($id));
$foo = unserialize(serializedFoo, ['allowed_classes' => [Foo:class]]);

Commandes Symfony

Quelques commandes Symfony utile pour manipuler notre cache :

  • Supprimer tout le contenu d’un cache pool : bin/console cache:pool:clear app.cache.mypool
  • Supprimer un item spécifique à l’aide de sa clé : bin/console cache:pool:delete app.cache.mypool <key>
  • Supprimer le contenu de tous les cache pool : bin/console cache:pool:prune

Conclusion

L’utilisation du cache pool et des Adapters n’est pas obscure et compliquée ; faire un service permettant de manipuler des données en cache est simple à mettre en place et permet de mieux comprendre le fonctionnement de ce composant ainsi que du cache plus généralement. Cela vous permettra d’obtenir des performances et des temps de chargement réduits pour l’affichage de vos pages.

Documentation

Voir plus

Retour du Symfony Live Paris, édition 2020

Lire l'article
Stéphane Meaudre

Stéphane Meaudre

Développeur PHP à Kaliop depuis 2014 et passionné d'astronomie, j'aime réfléchir à l'architecture et à la conception de mon code avant tout. C'est pour ces raisons que j'apprécie fortement de travailler avec Symfony et API Platform.

Commentaires

Ajouter un commentaire

Votre commentaire sera modéré par nos administrateurs

Vous avez un projet ? Nos équipes répondent à vos questions

Contactez-nous