Utiliser les tags Zend_Cache_Frontend_Page avec un site participatif

Il y a des domaines où le Zend Framework, en dépit de ses immenses possibilités, manque un peu de clarté. Si utiliser le frontend Page (qui met en cache l’intégralité d’une page en fonction de la requête : GET, POST, COOKIE, etc.) est particulièrement facile avec Zend_Cache, gérer efficacement la purge des pages est moins évident.

Sur un site institutionnel mis à jour peu fréquemment (une fois par jour) et à trafic modéré, la méthode classique de vidage complet du cache ($cache->clean()) peut être employée. Pendant les minutes qui suivent, les visiteurs du site subiront quelques ralentissements, le temps que le cache se reconstitue.

Mais sur un site participatif où les internautes publient des contenus des dizaines de fois par jour, cette méthode simple atteint ses limites. Pour améliorer les performances du cache, il devient important d’être sélectif dans l’invalidation des pages, en se restreignant uniquement à celles qui sont affectées par le changement. C’est ici qu’intervient le système de tags de Zend_Cache.

Les tags

Il est possible d’associer des tags spécifiques à chacune des pages ou urls définies dans le cache. On pourra ensuite supprimer, au moment de la mise à jour, uniquement les pages concernées. Supposons que vous ayez une page d’accueil listant des actus ainsi que les dernières questions d’utilisateurs, un moteur de recherche de questions, et des pages d’articles HTML qui changent rarement. Lorsqu’un utilisateur ajoute une question, on voudra mettre à jour l’accueil et les pages de résultats du moteur de recherche, mais pas les pages HTML. On définira donc des tags appropriés à chaque sous forme d’un tableau associatif, dans la déclaration des regexp :


$frontendOptions = array(

    // Options de base du cache de page,

    // Liste des pages avec leurs tags
    'regexps' => array(

        '^/$' => array(
            'cache' => true,
            'specific_lifetime' => 3600,
            'tags' => array('index','questions','actus')
            ),

        '^/reparations/liste' => array(
            'cache' => true,
            'tags' => array('questions')),

        '^/pages' => array(
            'cache' => true,
            'tags' => array('pages')),

        '^/[0-9]+/.*' => array(
            'cache' => true,
            'tags' => array('questions')
            )
    )
);

Lors de la mise à jour d’une fiche, on appellera, juste après avoir enregistré les modifications dans la base de données, la méthode de vidage du cache pour chaque tag :


// On sauve une question en BDD
// On suppose que le cache a été instancié dans le bootstrap 
// et est accessible dans la variable App::$cache
App::$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, 'index');
App::$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, 'liste');

Remarque : on peut passer un array de tags à la méthode clean(), mais elle fonctionne en mode AND et non OR : seules seront supprimées les pages vérifiant TOUS les tags, et non la liste de toutes les pages vérifiant UN tag. Pour supprimer toutes les pages répondant à une liste de tags, il faut appeler la méthode plusieurs fois. En ce qui me concerne, je règle cette question en l’appelant via une méthode App::viderCache($tags) qui prend un array en argument et les supprime un par un.

Un cache plus sélectif ?

Vous pourrez, à juste titre, trouver que ce système d’invalidation de cache reste très approximatif. Après tout, si notre site comporte 100000 questions, remettre à jour le tag questions va purger tous ces contenus d’un coup, ce qui n’est pas très efficace. Je ne crois pas que Zend_Cache propose un système plus précis via les Regexp, mais on peut ruser en segmentant à la main ses enregistrements. Par exemple, vous pouvez parfaitement créer une règle qui concerne toutes les questions dont l’identifiant se termine par le chiffre 1, et lui affecter le tag question_1, puis ceux qui terminent par 2 auront question_2, etc. jusqu’à 0. En regexp, on aura quelque chose comme ça (à tester) :


'regexps' => array(
    '^/[0-9]*1/.*' => array(
        'cache' => true,
        'tags' => array('questions_1')
    ),
    '^/[0-9]*2/.*' => array(
        'cache' => true,
        'tags' => array('questions_2')
    ),
    etc.
);

Lorsqu’on voudra invalider le cache après avoir mis à jour ou créé une question, on pourra donc appeler la fonction clean() avec le tag correspondant au dernier chiffre de l’identifiant de cette question. Ainsi à chaque mise à jour, on ne touchera qu’à 1/10ème des caches. Mais on peut aller encore plus loin : il y a peut-être une limite au nombre de regexps des Frontends Zend, mais je pense qu’on peut sans problème utiliser les 2 derniers chiffres en itérant de 00 à 99 (en traitant séparément les enregistrements de 1 à 9) avec une boucle for qui ajoute des lignes au tableau regexps et génère les tags appropriés :


$regexps = array(

    // Pages de base

);

for ($i = 0; $i <= 99; $i++)
{

    if (strlen($i) == 1)
    {
        $regexps[] = array(
            '^/' . $i . '/.*' => array(
                'cache' => true,
                'tags' => array('questions_' . $i)
            )
        );

    }
    $ids = str_pad($i, 2, '0', STR_PAD_LEFT);

    $regexps[] = array(
        '^/[0-9]*' . $ids . '/.*' => array(
            'cache' => true,
            'tags' => array('questions_' . $ids)
        )
    );
)

Les plus aventureux pourraient même tester un regexp à 3 chiffres, ou pourquoi pas couvrir l’intégralité des identifiants de la base (1 à 5000, 1 à 10000 ?), mais à partir d’une certaine taille le chargement du cache risque de devenir coûteux en ressources mémoire et CPU. Il faut trouver le bon compromis pour avoir un rechargement assez régulier mais sans trop multiplier les tags.

Le cas des codes Google Analytics/Google Adsense

Si vous utilisez des cookies pour authentifier des utilisateurs et que vous voulez bénéficier du cache, vous souhaitez probablement que chaque utilisateur ait une page en cache adaptée à son profil (compte, informations personnelles, contenus personnalisés, etc.). Dans ce cas vous activerez l’option cache_with_cookies_variables et make_id_with_cookie_variables du Frontend. Malheureusement, les scripts Google Analytics et Google Adsense génèrent leurs propres cookies pour TOUS les utilisateurs, ce qui fait que le cache est régénéré même pour les utilisateurs non authentifiés (qui devraient tous avoir le même contenu sur une url donnée). Pire, ces cookies changent de valeur à chaque rechargement de page, si bien que le cache est invalidé à chaque fois.

Un blog a identifié ce problème et propose une solution que j’ai adaptée : créer une classe Frontend_Page personnalisée qui ne tient pas compte des cookies Google. Cette classe hérite de Zend_Cache_Frontend_Cache et redéfinit uniquement la méthode _makePartialId() qui boucle sur les différentes variables pour créer un identifiant unique. Il suffit d’éliminer les Cookies commençant par un double underscore (__utmz, __utma, etc.) pour qu’ils ne soient pas pris en compte dans la création de l’identifiant unique :


// Partie à modifier dans la méthode _makePartialId()
case 'Cookie':
    if (isset($_COOKIE)) {
     $my_cookie = $_COOKIE;
     foreach($my_cookie as $key=>$val){
        // remove google analytics cookie
        if(false !== strpos($key,"__")){
            unset($my_cookie[$key]);
        }

     }
     $var = $my_cookie;
    } else {
      $var = null;
    }
    break;

Conclusion

En dépit de ces limitations, le cache de pages est un outil pratique, car il couvre l'ensemble d'un site sans nécessiter de s'intercaler dans chacun des contrôleurs de l'application (sauf lors de l'invalidation, bien sûr). On lui pardonnera donc son côté un peu trop radical. Pour un travail d'orfèvre (cache par page, par requête SQL), on pourra lui préférer d'autres Frontends comme Zend_Cache_Frontend_Output ou Zend_Cache_Core. Mais il faudra le déployer partout dans l'application, en plus de définir les tags, ce qui est plus laborieux. A vous de voir.

Code complet

Dans le bootstrap (class App) :


class App {

    public $cache;

    // functions d'instanciation du MVC, DB, etc.

    /**
    * Active le cache de pages
    *
    */
    public function usePageCache()
    {

        $debug_cache = true; // Activé en développement, pas en production
        $frontendOptions = array(
            'lifetime' => 86400,
            'cache_id_prefix' => 'cr_',
            'automatic_serialization' => true,
            'debug_header' => $debug_cache,
            'automatic_cleaning_factor' => 100, // Une fois sur 100, il vide les caches obsolètes automatiquement

            // On désactive le cache par défaut, on le réactivera plus tard par Regexp
            'default_options' => array(
                'cache' => false,
                'cache_with_get_variables' => true,
                'cache_with_post_variables' => false,
                'cache_with_session_variables' => true,
                'cache_with_files_variables' => false,
                'cache_with_cookie_variables' => true, // Uniquement si vous voulez cacher les pages des utilisateurs loggés
                'make_id_with_get_variables' => true,
                'make_id_with_post_variables' => true,
                'make_id_with_session_variables' => true,
                'make_id_with_files_variables' => true,
                'make_id_with_cookie_variables' => true,// Pour les utilisateurs loggés

            ),
            // On ne cache que les pages correspondant aux URL ci-dessous
            'regexps' => array(

                '^/$' => array(
                    'cache' => true,
                    'specific_lifetime' => 3600,
                    'tags' => array('index','questions','actus')
                    ),

                '^/reparations/liste' => array(
                    'cache' => true,
                    'tags' => array('questions')),

                '^/pages' => array(
                    'cache' => true,
                    'tags' => array('pages')),

                '^/[0-9]+/.*' => array(
                    'cache' => true,
                    'tags' => array('questions')
                    )
            )
        );
        $backendOptions = array(
            'cache_dir' => ROOT_PATH . '/tmp'
        );

        $front = new My_CacheFrontendPageGA($frontendOptions);
        App::$cache = Zend_Cache::factory(
            $front,
            'File',
            $frontendOptions,
            $backendOptions
        );

        App::$cache->start();


    /**
    * Vide le cache : intégralement ou selon une liste de tags
    *
    * @param array $tags
    */
    function viderCache($tags = null)
    {
        if (is_array($tags))
        {
            foreach ($tags as $tag)
            {
                $tag = array($tag);
                App::$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, $tag);
            }
        }
        else
        {
            App::$cache->clean();
        }
    }

Fichier de classe library/My/CacheFrontendPageGA.php

<?php
class My_CacheFrontendPageGA extends Zend_Cache_Frontend_Page {

    /**
     * Make a partial id depending on options
     *
     * @param  string $arrayName Superglobal array name
     * @param  bool   $bool1     If true, cache is still on even if there are some variables in the superglobal array
     * @param  bool   $bool2     If true, we have to use the content of the superglobal array to make a partial id
     * @return mixed|false Partial id (string) or false if the cache should have not to be used
    */
    protected function _makePartialId($arrayName, $bool1, $bool2)
    {
        switch ($arrayName) {
        case 'Get':
            $var = $_GET;
            break;
        case 'Post':
            $var = $_POST;
            break;
        case 'Session':
            if (isset($_SESSION)) {
                $var = $_SESSION;
            } else {
                $var = null;
            }
            break;
        case 'Cookie':
            if (isset($_COOKIE)) {
              $my_cookie = $_COOKIE;
              foreach($my_cookie as $key=>$val){
                 // remove google analytics cookie
                 if(false !== strpos($key,"__")){
                     unset($my_cookie[$key]);
                 }

              }
              $var = $my_cookie;
           } else {
               $var = null;
           }
           break;
        case 'Files':
            $var = $_FILES;
            break;
        default:
            return false;
        }
        if ($bool1) {
            if ($bool2) {
                return serialize($var);
            }
            return '';
        }
        if (count($var) > 0) {
            return false;
        }
        return '';
    }

}

Outil de référencement professionnel - essai gratuit Ce contenu a été publié dans Développement PHP, avec comme mot(s)-clé(s) , , , . Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *