Rafraîchir le cache de Symfony en mode no-debug

Le problème

Récemment je me suis retrouvé à devoir développer un bundle gérant des traductions en base de données pour Symfony ≥ 2.8. Une des demandes était de pouvoir ajouter ou éditer des traductions en production et voir ces dernières immédiatement prises en compte. Le soucis, c’est que les traductions sont mises en cache.

Aucun cache n’est rechargé en mode no-debug. Comme l’environnement de production utilise ce mode, aucun cache n’est rechargé en production.

Le pourquoi

Si vous jetez un coup d’œil aux fichiers dans le cache de Symfony, vous pourrez voir que certains portent le même nom à l’exception d’un suffixe .meta ; ceux-ci sont gérés par un ResourceCheckerConfigCache. Le concept est que chaque fichier mis en cache est généré à partir de « ressources », et ce sont ces dernières qui sont sérialisées dans les fichiers meta.

En mode debug des implémentations de ResourceCheckerInterface vérifient la validité de chaque ressource. Si une ressource est invalide alors le cache est invalide et doit être regénéré ; c’est ce qui permet de ne pas avoir besoin de le vider lorsque vous développez.

En mode no-debug le cache est considéré comme étant toujours valide, mais comment est-ce implémenté ? De manière très simple en vérité : les services implémentant ResourceCheckerInterface s’enregistrent comme tel via le tag config_cache.resource_checker pris en charge par la ConfigCachePass du FrameworkBundle. Un petit tour dans ce dernier nous montre que cette compiler pass n’est ajouté qu’en mode debug.

if ($container->getParameter('kernel.debug')) {  
    // …
    $container->addCompilerPass(new ConfigCachePass());
}

Le rôle de la ConfigCachePass est de modifier la définition du config_cache_factory pour construire une instance de ResourceCheckerConfigCacheFactory consciente de ces services.

public function process(ContainerBuilder $container)  
{
    $resourceCheckers = $this->findAndSortTaggedServices('config_cache.resource_checker', $container);

    if (empty($resourceCheckers)) {
        return;
    }

    $container->getDefinition('config_cache_factory')->replaceArgument(0, $resourceCheckers);
}

La solution

Le but du jeu va donc être d’imiter cette compiler pass mais uniquement en mode no-debug et en passant à config_cache_factory uniquement les checkers voulus.

Exemple

Dans mon cas les traductions sont stockées en base, j’ai donc créé le service result_cache_resource_checker qui va interroger le result cache de Doctrine.

Bundle

class TranslationBundle extends Bundle  
{
    /**
     * {@inheritdoc}
     */
    public function build(ContainerBuilder $container)
    {
        if (!$container->getParameter('kernel.debug')) {
            $container->addCompilerPass(new ResourceCheckerPass());
        }
    }
}

Compiler pass

class ConfigCachePass implements CompilerPassInterface  
{
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->getParameter('translation.force_check')) {
            return;
        }

        $configCacheFactoryDefinition = $container->getDefinition('config_cache_factory');

        $configCacheFactoryDefinition->replaceArgument(0, array_merge(
            $configCacheFactoryDefinition->getArgument(0),
            [new Reference('result_cache_resource_checker')]
        ));
    }
}

Notez le paramètre translation.force_check : j’ai désactivé cette fonctionnalité par défaut car elle impactera le temps d’exécution en production, on doit donc être sûr de vouloir l’activer.

Notez également que la condition se trouve dans la compiler pass et pas dans le bundle car le paramètre provient de la configuration et n’est donc pas accessible à ce moment.

Et… voilà ! J’espère que le cache de Symfony vous paraîtra moins opaque maintenant !