Théorie

Symfony utilise une implémentation bien connue de l‘interface PSR-6. Elle repose sur 3 concepts :

  • item : une unité d’information identifiée par une paire key/value dans laquelle key est un identifiant unique et valeur l’information elle même (par exemple un tableau, un objet ou une chaîne). Dans notre application, il s’agit d’un objet qui doit implémenter CacheItemInterface.
  • pool : on peut voir ça comme un dépôt qui contient tous nos items. De plus c’est lui qui va réaliser toutes les opérations basiques liées au système de cache (ajout, suppression, mise à jour  etc). Dans notre application, il s’agit d’un objet qui doit implémenter CacheItemPoolInterface.
  • adapter : la « glue » qui fait la liaison entre notre application et le cache. Dans notre application il s’agit d’un service et c’est lui qu’on va utiliser.

On a à notre disposition toute une série d’adapter, qui implémentent tous la même interface (AdapterInterface). L’utilisation d’un adapter plutôt qu’un autre est donc totalement transparent dans notre application. La différence sera l’endroit où est stockée l’information (fichier, base de données etc).

Opérations basiques

Pour terminer la théorie, voici un petit concentré d’utilisation du cache, en supposant que $cache soit le service de cache (ou adapter) :

// Create a new item by trying to get it from the cache
$myAwesomeItem = $cache->getItem('items.awesome');

// Assign a value to the item and save it
$myAwesomeItem->set('Am I cached ?');
$cache->save($myAwesomeItem);

// Retrieve the cache item
$myAwesomeItem = $cache->getItem('items.awesome');
if (!$myAwesomeItem->isHit()) {
    // ... looks like item does not exist in the cache
}
// retrieve the value stored by the item
$value = $myAwesomeItem->get();

// remove the cache item
$cache->deleteItem('items.awesome');

Services

Maintenant que nous en savons un peu plus, listons les services disponibles dans Symfony qui pourraient nous aider :

$ ./bin/console debug:autowiring cache

Psr\Cache\CacheItemPoolInterface                  
    alias to cache.app                            
Psr\SimpleCache\CacheInterface                    
    alias to cache.app.simple                     
Symfony\Component\Cache\Adapter\AdapterInterface  
    alias to cache.app 

On retrouve bien nos interfaces précédentes. Le service cache.app (qu’on retrouve d’ailleurs dans le profiler) va pouvoir être utilisé dans nos contrôleurs tout à fait classiquement :

use Symfony\Component\Cache\Adapter\AdapterInterface;

class FooController extends AbstractController
{
    /**
     * @var AdapterInterface
     */
    private $cache;

    public function __construct(AdapterInterface $cache)
    {
        $this->cache = $cache;
    }

    public function index(): Response
    {
        // Let's caching some nice items !
    }
}

Ensuite, voici un exemple d’utilisation hyper simple :

public function index(): Response
{
    $cache = $this->cache;
    $myAwesomeString = 'I want to be cached !';
    $key = 'awesomestring_' . md5($myAwesomeString);

    // Try to fetch item from cache
    $item = $cache->getItem($key);

    // Somehow it was not found in cache
    if(!$item->isHit()) {
        sleep(5);
        $item->set($myAwesomeString);
        $cache->save($item);
    }
}

On simule une occupation serveur de 5s avec la fonction sleep. Ce qui ce traduit par un affichage assez long de la page (5000ms). Petit tour dans le profiler > section cache, on a deux appels au service app.cache :

  • getItem() qui permet de créer l’objet
  • save() qui enregistre l’objet dans le cache puisqu’il n’existait pas

Maintenant on actualise la page, et c’est là que la magie opère : à peine 200ms de temps d’exécution (à moins que votre serveur ne soit un Pentium II 400, ça devrait tourner en dessous de 500ms) ! Petit tour dans le profiler > section cache, on a plus qu’un appel sur le cache : getItem(), ce qui est plutôt logique puisque l’objet a été créé juste avant.

Configuration

La première chose à faire est de consulter la configuration existante :

$ ./bin/console debug:config framework

Et éventuellement la configuration par défaut :

$ ./bin/console config:dump framework

Concernant la configuration existante, vous devriez avoir quelque chose d’approchant :

framework:
    cache:
        # Adapter used by the cache.app service
        app: cache.adapter.filesystem
        # Adapter used by the cache.system service
        system: cache.adapter.system
        # Directory used by cache.adapter.filesystem adapter
        directory: /home/xblog/www/xblogv7/var/cache/dev/pools
        default_redis_provider: 'redis://localhost'
        default_memcached_provider: 'memcached://localhost'
        pools: {  }

Symfony utilise donc deux services par défaut (on les appelle aussi des « pools ») :

  • cache.app est le service utilisé par votre application pour stocker vos données
  • cache.system est le service utilisé par les composants Symfony pour stocker leurs données (par ex. le Serializer, Validator metadata etc)

Pour configurer ces deux services, il suffit de renseigner les clés app et system . Par exemple, pour utiliser Apcu à la place de filesystem pour notre application, procédez comme suit :

framework:
    cache:
        prefix_seed: xblogv7
        app: cache.adapter.apcu

On peut vérifier que le service a changé en « dumpant » la variable :

public function __construct(AdapterInterface $cache)
{
    $this->cache = $cache;
    dump($this->cache);
}

On utilise bien le cache Apcu :

TraceableAdapter {
  #pool: ApcuAdapter
  -calls: []
}

Serveur de cache Redis

Le nec plus ultra en matière de cache est bien sûr l’utilisatoin d’un serveur dédié à cette fonction, ce qui permet d’allouer des ressources supplémentaires. Avant de continuer vous devez vous assurer que :

  • l’extension php Redis est activé dans le php.ini. Si ce n’est pas le cas, je donne des pistes ici
  • un serveur Redis local et opérationel

Vérifier que le serveur Redis écoute :

$ netstat -tupan | grep redis

Côté Symfony, modifier la configuration comme ceci :

framework:
    cache:
        prefix_seed: xblogv7
        app: cache.adapter.redis
        default_redis_provider: "redis://localhost"

En fait, ici, la clé default_redis_provider est la même que celle par défaut, ne la modifiez que si le serveur a une adresse différente. Dans ce cas vous devez utiliser une chaîne DNS valide, à savoir :

redis://[user:pass@][ip|host|socket[:port]][/db-index]

Maintenant si on dump la variable $cache comme précédemment :

TraceableAdapter {
  #pool: RedisAdapter
  -calls: []
}

L’extension Redis

Voici la procédure pour compiler l’extension Redis à condition d’avoir déjà compilé php avant et d’avoir toujours les sources à disposition. Tout d’abord se placer dans le répertoire des extensions (par exemple /usr/local/phpfarm/src/php-7.2.0/ext). Installer l’extension :

git clone https://github.com/phpredis/phpredis.git
cd phpredis
/usr/local/phpfarm/inst/php-7.2.0/bin/phpize
./configure --with-php-config=/usr/local/phpfarm/inst/php-7.2.0/bin/php-config
make
make test
make install

Bien sûr remplacez les chemins par les vôtre.

Désactiver le cache en développement

Depuis Symfony 4.1, il est possible de désactiver le cache en mode développement en utilisant l’adapter cache.adapter.array : il suffit de créer le fichier config/packages/dev/framework.yaml avec ce contenu :

framework:
    cache:
        app: cache.adapter.array

Références externes