Créer son premier module Drupal

Une des grandes forces de Drupal réside en son architecture à base de modules. Que ce soit pour la gestion des blogs ou celle d’un forum, chaque fonction fondamentale est en réalité un simple module interagissant avec le cœur de Drupal. Et si les modules fournis en standard ne suffisent pas, des centaines d’autres sont disponibles couvrant à peu prés tous les usages.

Mais malgré cette richesse, il arrive parfois que l’on ne trouve pas LE module « qui va bien ». Alors pourquoi ne pas le fabriquer soi-même et ainsi découvrir à quel point Drupal s'adapte facilement à des besoins spécifiques.

L'ensemble des sources de ce tutoriel est disponible via subversion.

Prérequis

Les seuls prérequis pour développer un module sont une bonne connaissance des concepts clés de Drupal (blocks, node, type de contenu, taxonomy, etc.), une relative maîtrise de PHP et quelques bases en SQL.

Armé de tout cela, nous allons commencer en douceur avec la création d'un module dont le seul but est de nous fournir un bloc contenant une liste de statistiques sur les contenus et les commentaires. Et tout cela pour Drupal 6.x.

Des modules et des hooks

Avant de poursuivre, il est important de comprendre ce qu'est réellement un module. Un module Drupal est un bout de code PHP que l'on peut activer ou désactiver, et qui implémente des "hooks".

Un hook (en français, "Crochet") est un prototype de fonction dédié à une tâche spécifique, comme par obtenir la liste des blocs disponibles et le contenu de chacun d'entre eux.

Chaque module peut ainsi implémenter ce hook sous la forme d'une fonction ayant des paramètres calqués sur ceux du prototype. Dans le cas d'un bloc, le prototype se nomme hook_view:
prototype du hook 'hook_view'hook_block($op = 'list', $delta = 0, $edit = array())

Ainsi un module désirant implémenter ce hook, devra comporter le code suivant :
implémentation de 'hook_view' dans le module 'monModule'

  1.   function hook_block($op = 'list', $delta = 0, $edit = array()) {
  2. // Traitement à effectuer sur le module
  3.    }

Lorsque Drupal aura besoin d'obtenir la liste des blocs, il cherchera dans la liste de toutes les fonctions PHP disponibles, c'est-à-dire les fonctions contenues dans tous les modules activés, celles dont le nom commence par le nom d'un module et se termine par _block, le nom du hook.

La manière dont Drupal utilise un hook est assez simple. Pour rester sur notre exemple, lorsqu'il a besoin d'obtenir la liste des blocs, il va appeler tout les fonctions _block qu'il trouve, les unes après les autres, en leur passant en premier paramètre la chaîne list. Et réponse, chacune va donner un tableau contenant les noms et descriptions des blocs qu'elles prennent en charge.

Ensuite lorsque Drupal va devoir afficher un bloc en particulier, il effectue la même opération mais cette fois en passant en paramètre view suivi du nom du bloc.

Maintenant au delà du mode d'exécution des hooks de même type, chacun est spécifique tant par les paramètres qu'il reçoit que par les valeurs que Drupal s'attend à recevoir. Vous pouvez obtenir leur description dans la documentation de l'API Drupal.

Structure d'un module

Un module est donc un fournisseur d'implémentations de hooks. En conséquence, la majeure partie de son code va s'insérer dans les traitements de Drupal et étendre son comportement. Maintenant de manière plus prosaïque, un module est simplement un dossier qui contient au minimum deux fichiers.

Ce dossier doit porter le nom du module et doit être placé dans l'arborescence de Drupal, à un endroit qui soit approprié pour que ce dernier puisse le voir. Techniquement il est possible de les mettre dans modules/ mais vous seriez vite embêtés lors d'un changement de version. Il est donc préconisé de mettre cela dans un dossier sites/all/modules/mes_modules que vous créerez si nécessaire. Ainsi lorsque vous changez de version, il suffit de déplacer le dossier sites à sa place dans la nouvelle arborescence.

Dans ce dossier mes_modules, vous allez donc créer le sous-dossier du module lui-même, dans notre cas statistiques. A l'intérieur de celui-ci, vous devez avoir au minimum deux fichiers, portant le même nom de base que le dossier parent : statistiques .info et statistiques .module. Ce qui nous donne l'arborescence suivante :

  1.   modules
  2.   ... etc ...
  3.   site
  4.     all
  5.       modules
  6.         contributions
  7.         ...etc...
  8.         mes_modules
  9.            statistiques
  10.             statistiques.info
  11.             statistiques.module
  12.           ...etc...
  13.       themes
  14.     ...etc...

Fichier .info

Le fichier statistiques.info est un simple fichier texte contenant des informations sur le module comme son nom, sa description, la version de Drupal avec lequel il est compatible, etc. Ce qui nous donne pour notre exemple, le contenu suivant :
statistiques/statistiques.info

  1. name = "Statistiques"
  2. description = "Statistiques sur les contenus"
  3. package = karma-lab
  4. project = "statistiques"
  5. version = "6.x-0.1"
  6. core = 6.x

package indique dans quelle catégorie le module doit être affiché. name, description sont des informations textuelles qui seront affiché dans le panneau d'administration des modules. name reprend le nom du module (qui est aussi le nom du dossier). version indique quant à lui la version du module. C'est d'ailleur plus une norme qu'autre chose qui se lit "Module version 0.1 pour Drupal 6.x". Enfin core prévient Drupal que le module est seulement compatible avec sa version 6.x. Tout autre version de Drupal empêchera le module de s'activer.

Si nous avions eu besoin de définir une dépendance avec un autre module, nous aurions ajouté une clause de la forme dependencies[] = autre_module. Enfin, si vous désirez ajouter des commentaires, il suffit d'utiliser le symbole ; suivi du texte du commentaire.

Fichier .module

Le second fichier est le code PHP du module. C'est lui qui va héberger les hooks que nous souhaitons implémenter. Pour l'instant, nous allons faire simple et n'en mettre aucun. Notre fichier va donc contenir une simple balise de démarrage de code PHP :
statistiques/statistiques .module<?php


Voilà, nous avons maintenant un module aussi simple qu'inutile, que Drupal est cependant capable de voir et d'activer. Pour s'en convaincre, il suffit d'aller faire un tour en http://mon_site_drupal/?q=/admin/build/modules pour le voir apparaître dans la liste. Vous constatez que les informations affichées sont les champs name et description de notre fichier statistiques.info.

A ce stade nous pourrions donc activer notre module, mais ne le faites pas tout de suite, nous avons encore quelque chose à rajouter.

Implémentation de hook_bock

Il est maintenant temps d'implémenter notre premier hook. Comme nous l'avons vu plus haut, celui qui est responsable de la génération de block est hook_block. Il nous faut donc ajouter son implémentation à la suite, dans notre fichier statistiques.modules:
à ajouter à statistiques.module

  1. function statistiques_block($op = 'list', $delta = 0, $edit = array()) {
  2.   $blocks=array();
  3.  
  4.   // Enumeration des blocs disponibles
  5.   if ($op == 'list') {
  6.     $blocks[] = array(
  7.       'info' => t('Affichage des statistiques'));
  8.     return $blocks;
  9.   }
  10.  
  11.   // Génération du formulaire de configuration
  12.   else if ($op == 'configure' && $delta == 0) {
  13.     $form['statistiques_commentaires'] = array(
  14.       '#type' => 'checkbox',
  15.       '#title' => t('Afficher les commentaires ?'),
  16.       '#default_value' => variable_get('statistiques_commentaires', 0),
  17.     );
  18.     return $form;
  19.   }
  20.  
  21.   // Sauvegarde du formulaire de configuration
  22.   else if ($op == 'save' && $delta == 0) {
  23.     variable_set('statistiques_commentaires', $edit['statistiques_commentaires']);
  24.   }
  25.  
  26.   // Génération du contenu à  afficher pour le block
  27.   else if ($op == 'view') {
  28.     switch($delta) {
  29.       case 0:
  30.         $block = array(
  31.           'subject' => t('Statistiques'),
  32.             'content' => 'Un bloc vide');
  33.         break;
  34.     }
  35.     return $block;
  36.   }
  37. }

Le paramètre important de ce hook est $op. Lorsque Drupal a besoin de la liste des blocs disponible, il appelle cette fonction avec $op=='list'. Drupal s'attend alors à ce que la fonction renvoie un tableau associant un id (ici statistiques) à une définition de bloc. Cette définition est elle aussi un tableau associant une propriété du bloc à sa valeur. Cette organisation des données est un grand classique dans Drupal.

Ici nous renvoyons à Drupal un seul bloc ayant pour description (propriété info) affichage des statistiques. Notez l'utilisation de la fonction t(...) qui permet de prévoir une prise en charge ultérieure de la traduction du module. Pensez à utiliser systématiquement cette fonction pour tous les textes.


Vous pouvez dés maintenant voir le hook en action en allant sur l'URL http://mon_site_drupal?q=admin/build/block. Vous devriez alors le voir apparaître dans la liste des blocs désactivés.

Passons maintenant aux opérations $op==configure et $op==save. La première fabrique un mini formulaire Drupal. La seconde sauvegarde le résultat du formulaire modifié par l'utilisateur.

Le formulaire est construit dans la variable $form dont la structure de stockage est la même que pour $blocks. C'est à dire un double tableau associatif, contenant la liste par id des champs. Puis par champ, la liste des propriétés et leurs valeurs.

La propriété #type détermine le type du champ (zone de texte,liste déroulante, etc). Ici nous choisissions une case à cocher (checkbox). La liste de complète des types et de leurs propriétés associées est disponible dans la documentation de Drupal.

Mais la propriété vraiment intéressante est #default_value. Elle définit la valeur que dois prendre le champ au moment de l'affichage du formulaire. Cette valeur est extraite des propriétés générales de Drupal par la fonction variable_get(...). En effet, Drupal met à disposition des modules un système de variables persistantes très simple à mettre en oeuvre.

La dernière propriété #title définit le titre du champ. Là aussi nous utilisons la fonction t(...) en prévision d'éventuelles traductions à venir.


Notre micro formulaire est maintenant prêt à être utilisé par Drupal. Vous pouvez le tester en cliquant sur le lien configurer dans la page d'administration, à la fin de la ligne du bloc statistiques, après sa description.

Lorsque vous aurez paramétré votre bloc, le fait de cliquer sur le bouton Enregistrer le bloc provoquer un nouvel appel de notre hook, mais cette fois avec le paramètre save renvoyant les résultats du formulaire définit à l'étape précédente. Ces résultats sont contenus dans le tableau associatif $edit et notre seule tâche est d'utiliser la fonction Drupal permettant de stocker une variable persistante, variable_set(...);, en lui passant la valeur du champ statistiques_commentaires.

La dernière étape est la génération du code HTML du bloc. C'est le rôle de l'opération $op==view qui utilise le paramètre du hook $delta.

hook_block permettant de créer autant de blocs que désiré, $delta contient l'ID du bloc à créer. Ici nous n'en avons qu'un mais autant prévoir l'avenir et effectuer tout de même le test. Ensuite, nous définissons une fois de plus un tableau associatif contenant deux propriétés, le titre du node (subject) et le contenu HTML du node (content).


Il est maintenant temps d'afficher notre bloc en sélectionnant une position dans la page d'administration. Une fois ce paramétrage sauvegardé (bouton enregistrer les blocs, le nouveau bloc devrait apparaître là où vous l'avez décidé.

Contenu du bloc

Maintenant que notre bloc est prêt, passons à l'affichage de son contenu. Pour cela nous allons remplacer la ligne content' => 'Un bloc vide'); par 'content' => statistiques_contenu()); et ajouter la fonction de rendu HTML de notre bloc :
à ajouter à statistiques.module

  1. function statistiques_contenu() {
  2.   $result="<ul>";
  3.   $cursor = db_query("SELECT type,count(type) as count FROM {node} GROUP BY type");
  4.   while ($statistique=db_fetch_object($cursor)) {
  5.     $result.="<li>".t($statistique->type).' : '.$statistique->count."</li>";
  6.   }
  7.   if (variable_get('statistiques_commentaires', 0)) {
  8.     $comments = db_result(db_query("SELECT count(*) as count FROM {comments}"));
  9.     $result.="<li>".t("commentaires").' : '.$comments."</li>";
  10.   }
  11.   $result.="</ul>";
  12.   return $result;
  13. }

Un des gros avantage de Drupal est l'abstraction de la couche base de données qu'il propose. Elle est certes simple mais très efficace. Et du point de vue du module, peu importe que l'on utilise PostrgeSQL ou MySQL, Drupal lui fournit un jeu de fonctions permettant d'oublier ce "détail".

La fonction db_query a pour rôle de lancer la requête et de renvoyer un curseur sur le résultat. Elle permet l'utilisation des motifs %d et %s comme la fonction PHP sprintf.

Notez que db_query ne va pas automatiquement insérer de guillemets autour des chaînes. C'est donc à vous de mettre des '%s'. En revanche, elle se charge de l'échappement des caractères spéciaux vous protégeant des injections de code SQL malicieux, ce qui n'est déjà pas si mal.

La fonction db_fetch_object va lire en base un enregistrement à la position courante du curseur (celui renvoyé par db_query) et transformer le résultat en un objet PHP. L'objet reçoit ainsi deux champs type et count contenant les résultats de la requête.

La fonction db_result est une version simplifiée de db_fetch_object qui est utile lorsque la requête ne renvoie qu'une seule valeur.

Ensuite il s'agit d'un simple travail de formatage HTML du résultat des deux requêtes. Notez la conditions concernant les commentaires qui s'appuie sur la valeur stockée lors de la configuration du block.


Il ne reste maintenant plus qu'à apprécier le résultat.

Conclusion

Voilà, nous avons terminé notre premier module totalement fonctionnel en espérant que vous avoir montré à quel point, une fois quelques notions de base assimilées Drupal est facile à adapter à des besoins même très particuliers.