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');
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 '';
}
}
Google+Billets similaires
- Etendre un Zend_Db_Table_Row avec des propriétés customisées
- Déboguer les erreurs MySQL grâce aux alertes e-mail
- Tutoriel : sauvegarder quotidiennement ses bases de données MySQL sur un serveur dédié ou privé
- Tutorial PHP : chronométrer le temps de calcul des requêtes SQL (benchmark)
- Tutorial MySQL : alléger des requêtes successives avec CREATE TEMPORARY TABLE