Version imprimable Version PDF RSS
ven, 14/03/2008 - 11:58 | Yoran   Tutoriels Drupal
Changer le thème de drupal, avec le cache

Dans le volet précédent de la saga "Comment changer dynamiquement de thème Drupal, notre module souffrait d'un défaut de taille : Il fallait désactiver le cache pour qu'il fonctionne. Nous allons voir maintenant comment le modifier pour que la même chose soit possible sans perte de performances.

Cet article a été écrit pour Drupal 5, et n'est pas testé sous Drupal 6

Gestion du cache

Le cache de page sous Drupal est un mécanisme bête et efficace. S'il est actif, on commence avant d'afficher quoi que ce soit, par vérifier qu'il n'existe pas un enregistrement correspondant à l'URL demandée dans la table cache_page. Si c'est le cas, les données de cet enregistrement sont directement expédiées sur le navigateur client et le traitement s'arrête là. Le cache de page contient donc toute la page, thème compris. Et c'est bien là notre problème.

Ce cache peut être soit désactivé, soit en mode "normal", soit en mode "agressif". En mode agressif, aucun module ne peut intervenir et le site est quasiment "statique". C'est pour cela qu'il est assez rarement utilisé. En mode normal, certains modules dits "bootstrap" (voir l'article précédent) peuvent avoir une action sur le contenu, c'est ce que nous allons exploiter.

Bien évidement, une page mise en cache n'est pas valide à vie. Il y a un paramètre que vous pouvez modifier dans les performances indiquant de durée de validité des enregistrements. Passé ce temps, la page est refabriquée et vient écraser l'ancienne version en base.

Un autre aspect important à connaître sur ce cache de page, c'est qu'il est désactivé pour tout utilisateur connecté. Ce qui peut poser un problème de performance dans le cas où votre site n’a que des utilisateurs authentifiés. En gros, le cache n'agit que sur la version anonyme du site et c'est un aspect à ne pas négliger car lorsque l'on développe, nous sommes souvent connectés, et le comportement n'est plus du tout le même...

Donc pour créer un thème dynamique en fonction du navigateur cible, avec un cache en mode "normal", on va tomber sur le problème du "premier qui gagne". Si un navigateur IE6 passe avant les autres, c'est le thème IE6 qui va finir dans le cache et tout le monde sera logé à la même enseigne. Pire, vu qu'il y a un enregistrement par page, et donc à la rubrique/billet affiché, on aurait rapidement un site "arlequin" avec un thème qui change de manière aléatoire d'une page à l'autre.

La seule solution pour régler ce problème est donc de modifier la stratégie de gestion du cache.

Stratégie de cache

Malheureusement il n'y a pas de hooks que l'on puisse implémenter pour changer la manière dont fonctionne le cache. Il va donc nous falloir modifier les sources du coeur de Drupal. Prenez donc soin d'en faire une copie préalable.

Le principe général est assez simple. Drupal retrouve le bon enregistrement d'une page dans le cache en fonction d'une clef. Cette clef est simplement l'URL de la page à afficher. Il va donc nous falloir changer un peu cela pour que la nouvelle clef soit cette fois l'URL additionnée du thème $custom_theme.

La stratégie du cache de page est pris en charge dans le module includes/bootstrap.inc, dans la fonction _drupal_cache_init. C'est là que sont appelés nos kooks _init et _exit. Et c'est aussi là qu'est fait la distinction entre les trois modes du cache.

Le problème est que le hook _init est appelé après récupération de la page en cache. Et du coup notre clef va être fausse. Il faut donc modifier la fonction pour faire appel à notre module avant que le cache ne soit lu. Malheureusement il n'existe pas de fonction dans l'API de cache pour vérifier qu'un enregistrement existe avant de le charger effectivement. Et si l'on continue d'utiliser le hook _init, on prend le risque qu'il soit appelé deux fois de suite : une fois avant la lecture en cache, et une fois de plus s'il n'y avait pas d'enregistrement en cache pour cette page. Ce n'est pas un problème pour notre module mais cela risque d'en devenir un pour les autres modules "bootstrap" utilisant init/exit. Nous allons donc éviter le problème en créant notre propre hook, _preinit, et en l'invoquant avant la récupération des données en cache. Cela nous donne la fonction de remplacement suivante :

function _drupal_cache_init($phase) {
require_once variable_get('cache_inc', './includes/cache.inc');

if ($phase == DRUPAL_BOOTSTRAP_EARLY_PAGE_CACHE && variable_get('page_cache_fastpath', 0)) {
if (page_cache_fastpath()) {
exit();
}
}
elseif ($phase == DRUPAL_BOOTSTRAP_LATE_PAGE_CACHE) {
if (variable_get('cache', CACHE_DISABLED) == CACHE_AGGRESSIVE) {
if ($cache = page_get_cache()) {
drupal_page_cache_header($cache);
exit();
}
}
elseif (variable_get('cache', CACHE_DISABLED) == CACHE_NORMAL) {
require_once './includes/module.inc';
bootstrap_invoke_all('preinit');
if ($cache = page_get_cache()) {
bootstrap_invoke_all('init');
drupal_page_cache_header(page_get_cache());
bootstrap_invoke_all('exit');
exit();
}
return;
}
require_once './includes/module.inc';
}
}
includes/bootstrap.inc

Ensuite, nous allons modifier le module de l'article précédent pour renommer _init en _preinit, et créer une nouvelle fonction _init qui sera cette fois vide.

Lecture et écriture du cache

Maintenant nous allons modifier les fonctions qui lisent et qui écrivent dans le cache pour ajouter le thème dans la clef. La lecture tout d'abord, se trouve dans includes/bootstrap.inc, fonction page_get_cache. La modification est enfantine, elle consiste juste à déclarer en global notre variable $custom_theme et ajouter cette référence dans la clef de lecture du cache :

function page_get_cache() {
// { Premiere modification
global $user, $base_root, $custom_theme;
// }

$cache = NULL;

if (!$user->uid && $_SERVER['REQUEST_METHOD'] == 'GET' && count(drupal_set_message()) == 0) {
// { Deuxième modification
$cache = cache_get($base_root . request_uri()."___".$custom_theme, 'cache_page');
// }

if (empty($cache)) {
ob_start();
}
}

return $cache;
}
includes/bootstrap.inc, fonction page_get_cache

Enfin, il ne nous reste plus qu'à modifier la fonction de sauvegarde de la page, qui elle se trouve dans includes/common.inc, fonction page_set_cache. Et c'est le même type d'altération :

function page_set_cache() {
// { première modification
global $user, $base_root, $custom_theme;
// }

if (!$user->uid && $_SERVER['REQUEST_METHOD'] == 'GET' && count(drupal_get_messages(NULL, FALSE)) == 0) {
// This will fail in some cases, see page_get_cache() for the explanation.
if ($data = ob_get_contents()) {
$cache = TRUE;
if (function_exists('gzencode')) {
// We do not store the data in case the zlib mode is deflate.
// This should be rarely happening.
if (zlib_get_coding_type() == 'deflate') {
$cache = FALSE;
}
else if (zlib_get_coding_type() == FALSE) {
$data = gzencode($data, 9, FORCE_GZIP);
}
// The remaining case is 'gzip' which means the data is
// already compressed and nothing left to do but to store it.
}
ob_end_flush();
if ($cache && $data) {
// { seconde modification
cache_set($base_root . request_uri()."___".$custom_theme, 'cache_page', $data, CACHE_TEMPORARY, drupal_get_headers());
// }
}
}
}
}
includes/bootstrap.inc, fonction page_get_cache

Et voilà, c'est terminé. Il ne reste plus qu'à vider le "vieux" cache en passant par votre base de donnée et en exécutant un delete from cache_page. Maintenant, vous pouvez ré-activer le cache (en mode normal, pas agressif !!) et tester. En base de donnée, un select cid from page_cache vous confirme que les nouvelles clefs sont prise en compte.

Conclusion

Outre une meilleure compréhension du système de cache de Drupal, nous avons maintenant un module de changement dynamique de thème qui fonctionne parfaitement avec le cache, sans perte de performances. Le seul impacte est donc un doublement du volume du cache en base, ce qui n'est pas un problème en soit. Alors certes cela s'est fait au prix d'une modification mineure du Coeur mais le résultat est là. Maintenant il est toujours possible de rendre cette modification plus générique en introduisant un hook _get_cache_page_key qui prendrait en paramètre la clef d'origine et qui renverrait la clef modifiée. Cela réduirait considérablement l'impacte sur le coeur de Drupal. Pour une prochaine fois peut-être ;-)

Anonymous , le lun, 29/12/2008 - 05:04

Merci énormément pour ton article. Il est vrai qu'il ne répond pas directement à ma question, mais il m'a permis de trouver un élément qui me permettra (et qui m'a permis) de répondre à ma question.

Je garde le lien de la page, et je te fais signe dès que mes essais sont terminés. Cependant, ca risque prendre du temps, vu que je travaille pas trop sur ce projet (une ou deux heures par semaine).

Cependant, merci encore.

Yoran, le jeu, 01/01/2009 - 12:50

Ah bé ravis que cela puisse t'aider. La technique est un peu obsolète (Drupal 5) mais marchait très bien à l'époque où j'utilisais cette version. N'hésites pas si tu as besoins d'infos.

Publier un nouveau commentaire

Le contenu de ce champ sera maintenu privé et ne sera pas affiché publiquement. Si vous avez un compte gravatar, l'utilisez pour afficher votre avatar.
  • Les adresses de pages web et de messagerie électronique sont transformées en liens automatiquement.
  • Every instance of custom tags in the input text will be replaced with a specific tool shortcut.
  • To highlight piece of code, just surround them with <code type="language"> Your code &tl;/code>>. Language can be java,c++,bash,etc... Everything Geshi support.
  • Les lignes et les paragraphes vont à la ligne automatiquement.

Plus d'informations sur les options de formatage

Êtes-vous humain ?
Cette question est là pour déterminer si vous êtes humain ou pas...