Créer son propre module Drupal
Le 8 October 2008, à 15:2 par Ulhume...

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.

Ce gros tuto est encore en béta test Wink
L'ensemble des sources de ce tutoriel est disponible via subversion.
Historique (tout afficher)
  • v36 - divers corrections (2008-11-18 12:32)
  • v23 - début demodification de l'affichage (2008-05-21 01:06)

Où allons nous ?

Les prérequis pour développer un module sont évidemment une bonne connaissance des concepts clés de Drupal (node, type de contenu, taxonomy, etc.), une relative maîtrise de PHP et quelques bases en SQL. Armé de tout cela, nous allons dans ce tutoriel chercher à construire un module permettant de gérer une simple liste de courses. Il s'agira en quelque sorte de la version moderne du tableau sur le réfrigérateur, amélioré d'une petite gestion des réserves. Le module permettra :

  • L'enregistrement, la modification et la suppression d'un produit. Un produit étant défini par son nom, sa quantité en réserve et la valeur seuil devant déclencher son rachat.
  • L'affichage de la liste des produits. Chaque produit devant être racheté sera surligné. Sur chaque ligne, seront disposées deux actions, l'une permettant de le modifier, l'autre de décrémenter la quantité de produit restant d'une unité.

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 exemple mettre en forme le contenu d'un node avant affichage. 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 de la mise en forme, le prototype se nomme hook_view:

function hook_view($node, $teaser = FALSE, $page = FALSE)
prototype du hook 'hook_view'

Ainsi un module désirant implémenter ce hook, devra comporter le code suivant :

   function monModule _view($node, $teaser = FALSE, $page = FALSE) {
// Traitement à effectuer sur le module
   }
implémentation de 'hook_view' dans le module 'monModule'

Lorsque Drupal aura besoin de mettre en forme un node, il cherchera dans la liste de toutes les fonctions PHP disponibles, c'est-à-dire les fonctions contenues dans tous les modules activés, thèmes compris, celles dont le nom commence par le nom d'un module et se termine par _view, le nom du hook.

Ensuite, Drupal va appeler les fonctions les unes après les autres et chacune d'entre elles va modifier l'objet $node passé en paramètre.

Le hook hook_view est défini et utilisé par le module modules/node.module fournis en standard. Nous comprenons ainsi que tous les modules peuvent proposer leur propre hook pour permettre à d'autres modules de prendre en charge un traitement spécifique. Pour avoir la liste des hooks fournis par un module, il faut se reporter à sa documentation. Dans le cas spécifique des modules fournis en standard dans Drupal (node, book, etc), vous trouverez leur description dans la documentation de l'API Drupal. Bien l'étudier est important car chaque hook a son propre fonctionnement. Certains s'attendent à ce que le contenu de l'objet passé en paramètre soit modifié, d'autres qu'un tableau soit renvoyé par la fonction, etc.

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 en étendre le 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 courses. 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 : courses.info et courses.module. Ce qui nous donne l'arborescence suivante :

  modules
  ... etc ...
  site
    all
      modules
        contributions
        ...etc...
        mes_modules
          courses
            courses.info
            courses.module
          ...etc...
      themes
    ...etc...

Le fichier courses.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 :

name = "Liste des courses"
description = "Gestion de la liste des courses"
package = karma-lab
version = "5.x-1.0"
project = "courses"
courses/courses.info

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 :

courses/courses.module

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, comme sur l'illustration, apparaître dans la liste. Vous constatez que les informations affichées sont les champs name et description de notre fichier courses.info.

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

Installation

Notre module courses a pour finalité de pouvoir saisir des produits et de les présenter sous la forme d'une liste. Nous pourrions pour cela utiliser un type de contenu simple comme story mais cette structure de stockage serait trop limitée car nous avons des informations plus précises à saisir que les seuls titre et corps. Il nous faut en effet spécifier le nombre de produits restants et le nombre minimum à avoir en réserve pour pouvoir générer notre liste de courses. Nous allons donc créer un nouveau type de node, produit, en cherchant à étendre le type de node simple.

Drupal stocke les données de base d'un node (titre, corps, etc.) dans la table node dont la clef primaire est le champ nid. Ce sont les valeurs de ce champ que vous retrouvez dans l'URL d'un node, sous la forme http://mon_site_drupal?q=/node/1234. Elle dispose aussi d'un champ type de type texte identifie le type du contenu. Dans notre cas ce type prendra la valeur produit.

Mais pour stocker nos informations supplémentaires, nous allons devoir créer une nouvelle table, traditionnellement nommée node_produit, contenant un champ quantité pour la quantité restante du produit et un champ seuil pour la valeur minimum à conserver en réserve. En outre elle disposera, elle aussi, d'un champ nid. Les informations sur un produit donné seront donc réparties sur les champs des deux tables à la fois (node et node_produit)reliées entre-elles par le même nid.

Du coup, pour obtenir toutes les informations disponibles sur un node de type produit, nous pouvons formuler la requête suivante :

SELECT n.*,i.* FROM node n, node_produit i WHERE n.nid=i.nid AND n.type='produit';

Alors nous pourrions évidemment créer cette nouvelle table à la main mais ce serait bien peu pratique lorsque viendra le moment de redistribuer ce module. La bonne pratique est donc de déléguer ce travail à Drupal lors de l'activation du module. De la même manière, nous allons lui indiquer comment la supprimer, lorsque le module est désactivé puis désinstallé.

Pour ce faire, il nous faut rajouter un troisième fichier à notre dossier module : courses.install qui va contenir deux hooks. Le premier, hook_install, est invoqué lors de la première activation du module (installation). Le second, hook_uninstall, est quant à lui invoqué lors de la désinstallation du module.

<?php

/**
 * Implementation of hook_install().
 */

function courses_install() {
  $result= array ();
  switch ($GLOBALS['db_type']) {
    // Installation de la table pour MySQL
    case 'mysql' :
    case 'mysqli' :
      $result[]= db_query("
      CREATE TABLE {node_produit} (
    nid int(10),
        quantite int(10),
        seuil int(10),
        PRIMARY KEY (nid)
        )"
);
      break;

    // Installation de la table pour PostgreSQL
    case 'pgsql' :
      error_log("install on Psql");
      $result[]=db_query("
    CREATE TABLE {node_produit} (
          nid INTEGER,
          quantite INTEGER,
          seuil INTEGER,
          PRIMARY KEY (nid)
         );"
);
      break;
  }
  return $result;
}

/**
 * Implementation of hook_uninstall().
 */

function courses_uninstall() {
  $result=array();
  $result[]=db_query('DROP TABLE {node_produit}');
  return $result;
}
courses/courses.install

Le code n'a rien de compliqué mais quelques points cependant méritent éclaircissement.

Tout d'abord, nous distinguons deux cas qui correspondent aux deux bases de données prises en charge par Drupal : mySQL et PostgreSQL. En effet, si les bases de données sont à peu près équivalentes s'agissant des requêtes INSERT,DELETE,UPDATE et SELECT, c'est un peu moins le cas pour les commandes CREATE et ALTER.

Ensuite concernant l'exécution des requêtes SQL, nous devons à tout prix éviter d'utiliser les fonctions base de données de PHP au profit de celles fournies par Drupal. En effet ces dernières présentent l'avantage de faire abstraction de la base de données sous-jacente.

Dans le cas spécifique de l'installation et de la désinstallation, nous utilisons update_sql mais pour le reste du code elle sera remplacée par db_query. Ces deux fonctions sont pratiquement identiques à la différence près qu'update_sql fournit plus d'informations sur le déroulement de la requête. Informations qui seront utilisées par l'assistant d'installation et de mise à jour de Drupal.

Dernier point : les accolades utilisées pour encadrer le nom des tables. Elles ne sont pas nécessaires mais permettent à Drupal certaines opérations de préparation des requêtes. C'est donc juste une bonne habitude à prendre.

Maintenant que notre installation est finalisée, il ne nous reste plus qu'à activer notre module dans le panneau d'administration de Drupal : http://mon_site_drupal?q=/admin/build/modules. Une fois cette opération achevée, vous pouvez vérifier à l'aide de votre explorateur de base de données préféré que la table node_produit a bel et bien été créée. Si vous désactivez et réactivez votre module, notez que le code d'installation n'est pas ré-exécuté. En effet, il vous faut d'abord désinstaller le module, pour que le hook courses_uninstall soit appelé et détruise la table. Ensuite, si vous réactivez à nouveau le module, la table sera recréée.

Type de contenu

Notre base est prête, notre module activé et pourtant si nous allons sur l'URL http://mon_site_drupal/?q=node/add, notre produit n'apparaît pas. En effet, il faut implémenter deux hooks supplémentaires dans courses.module pour indiquer à Drupal qu'un nouveau type de node est à prendre en charge. Le premier hook, hook_node_info, va décrire le nouveau type de node :

function courses_node_info() {
  return array(
    'produit' => array(
      'name' => t('Un produit'),
                'description' => t("Un produit dans la liste des courses."),
      'module' => 'courses'));
}
à ajouter au fichier courses.module

Ce hook permet de renvoyer à Drupal une liste de nouveaux types de contenu. Ici nous n'en indiquons qu'un seul, produit, avec sa description et son nom usuel. L'identifiant produit sera utilisé dans la colonne type de la table node comme vu au chapitre précédent. Notez enfin l'utilisation de la fonction t(...) qui permet à Drupal de prendre en charge l'affichage d'une phrase en plusieurs langues. Pensez à utiliser systématiquement cette fonction pour tous les textes.

Le dernier paramètre correspond au nom du module qui va gérer ce type de contenu, en l'occurrence notre module courses<.kbd>.

Formulaire de saisie

Une dernière étape est nécessaire à l'apparition dans la liste de notre nouveau type de contenu : mettre en place le formulaire de saisie. Mais avant cela, il est important de bien comprendre comment Drupal fait vivre un node.

Un node est représenté dans le code de Drupal par l'objet $node. A chaque étape du cycle de vie du node, différents hooks viennent enrichir cet objet jusqu'à y ajouter le code HTML qui sera inséré dans la page. C'est ainsi que dans le template node.tpl.php, l'affichage du titre passe par une référence à $node->title.

Il existe une relation étroite entre l'objet $node et sa représentation en base de donnée. Si le node existe déjà, Drupal transfère les valeurs correspondantes de la table node vers les champs de l'objet $node. Les identifiants de chaque colonne deviennent ainsi les noms d'attributs de l'objet. Lorsque le node n'existe pas encore, Drupal crée simplement un objet vide. C'est donc soit l'objet vide, soit l'objet chargé des valeurs en base, que nous allons récupérer dans le hook qui va créer notre formulaire, hook_form.

Ce hook est chargé de créer le formulaire mais aussi de l'initialiser avec les valeurs de l'objet $node. Ce formulaire sera soit utilisé sur un objet vide ou soit sur contenant les données d'un contenu existant.

function courses_form(& $node)
{
  $form['title']= array (
    '#type' => 'textfield',
    '#title' => t("Nom du produit"),
    '#default_value' => $node->title,
    '#required' => TRUE
  );
  $form['body_filter']['body']= array (
    '#type' => 'textarea',
    '#title' => t('Notes'),
    '#description'=>t("Notes sur le produit"),
    '#default_value' => $node->body,
    '#rows' => 10,
    '#required' => FALSE);
  $form['body_filter']['format']= filter_form($node->format);

  $form['seuil'] = array(
    '#type' => 'textfield',
    '#title' => t("Seuil d'achat"),
    '#description'=>t("Quantité minimum en dessous de laquelle il faut racheter le produit"),
    '#size' => 4,
    '#required' => TRUE,
    '#default_value' => $node->seuil
  );


  $form['quantite'] = array(
    '#title' => t("Quantité"),
    '#description'=>t("Quantité de produit restant en réserve"),
    '#type' => 'textfield',
    '#size' => 4,
    '#required' => TRUE,
    '#default_value' => $node->quantite
  );

  return $form;
}
Corps de la fonction courses_form

Un formulaire Drupal est tableau $form dont chaque entrée correspond à la définition d'un des champs du formulaire ($form['champ']=...).

La définition du champ en elle-même est tableau associant une propriété (#title, #required, etc) à sa valeur. La proprété #type détermine le type du champ (case à cocher, zone de saisie, etc). La liste de ces types et de leurs propriétés associées est disponible dans la documentation de Drupal.

La propriété vraiment intéressante ici est #default_value, car c'est elle qui va permettre d'extraire de $node, les valeurs que le formulaire doit permettre de modifier. Lorsque l'utilisateur validera le formulaire, Drupal se chargera de réintégrer automatiquement les valeurs saisies dans l'objet $node. C'est pour cela que l'identifiant du champ est le même que celui de l'attribut correspondant dans $node ($form['quantite'] et $node->quantite).

Comme nous l'avons vu plus haut, l'objet $node est vide à sa création. Si nous l'utilisons tel quel, les champs quantité et seuil seront vides (et non égal à 0). Il est donc nécessaire d'implémenter un nouveau hook, hook_prepare, qui a pour rôle de préparer l'objet $node avant utilisation dans le formulaire.

function courses_prepare(& $node) {
  $node->quantite=$node->quantite?$node->quantite:0;
  $node->seuil=$node->seuil?$node->seuil:0;
}
à ajouter au fichier courses.module

Grâce à cette fonction, nous initialisons notre objet $node avec les bonnes valeurs par défaut. De la même manière nous pouvons valider ce que renvoie le formulaire en implémentant cette fois hook_validate

function courses_validate($form_id, $form_values) {
  if (!is_numeric($form_values['quantite']) || $form_values['quantite'] < 0) {
    form_set_error('quantite', t('Le champ quantité doit être une valeur numérique et positive'));
  }
  if (!is_numeric($form_values['seuil']) || $form_values['seuil'] < 0) {
    form_set_error('seuil', t('Le champ seuil doit être une valeur numérique et positive'));
  }
}
<code>

 
  Ici nous cherchons juste à nous assurer que les valeurs saisies sont numériques et positives. Maintenant notre formulaire est complet. Nous pouvons l'essayer en utilisant l'URL <kbD>http://mon_site_drupal?q=/node/add/produit</kbd>.
 

<h2>Base de données</h2>
 
Nous avons maintenant un objet <kbd>$node</kbd> correctement renseigné par notre formulaire. Il ne nous reste donc plus qu'à coder sa sauvegarde en base de données. Pour ce faire, nous allons devoir implémenter quatre hooks, un par commande SQL.


 Commençons par le plus simple : la lecture. Elle est prise en charge par le hook <kbd><external href="http://api.drupal.org/api/function/hook_load/5">hook_load</external><kbd> :
<code type="php" caption="à ajouter à courses.module">
function courses_load($node)
{
  return db_fetch_object(db_query('
SELECT * FROM {node_produit} WHERE nid = %d', $node->nid));
}
à ajouter au fichier courses.module

hook_load reçoit en paramètre un objet $node contenant déjà des informations extraites de la table node. Cela inclut le nid que nous utilisons dans notre requête pour lire en base nos valeurs supplémentaires. L'objet ainsi formé est renvoyé à Drupal qui l'agrégera à l'objet $node.

db_query, dont nous parlions au chapitre Installation, renvoie un curseur sur le résultat de la requête qu'elle vient d'exécuter. Elle autorise l'utilisation des motifs %d et %s comme la fonction PHP sprintf. Cela nous permet d'éviter d'encombrer la requête et ainsi gagner en lisibilité.

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.

Enfin, la fonction Drupal db_fetch_object va lire un enregistrement à la position courante du curseur et transformer le résultat en un objet PHP sous la forme $object->identifiant_colonne=valeur_colonne.

Voilà qui est terminé pour le chargement des données de notre nouveau type de contenu. Voyons maintenant comment insérer, mettre à jour ou encore détruire nos données supplémentaires en implémentant respectivement hook_insert, hook_update et hook_delete :

function courses_insert($node)
{
  db_query("INSERT INTO {node_produit} (nid, quantite, seuil) VALUES (%d, %d, %d)", $node->nid, $node->quantite, $node->seuil);
}

function courses_update($node) {
  db_query("UPDATE {node_produit} set quantite=%d, seuil=%d WHERE nid=%d", $node->quantite, $node->seuil, $node->nid);
}


function courses_delete(&$node) {
  db_query('DELETE FROM {node_produit} WHERE nid = %d', $node->nid);
}
à ajouter à courses.module

Notre nouveau type de contenu implémente maintenant tous les hooks nécessaires à son stockage en base de données. Nous pouvons maintenant créer un nouveau produit, saisir ses valeurs, le sauvegarder, le modifier, et le détruire.

Affichage

La première chose que l'on constate une fois que l'on a saisi et validé notre premier produit est que les informations affichées n'incluent ni la quantité, ni le seuil de rachat. Ne sont présentes que les données d'un node standard. Pour changer cela, nous allons implémenter un nouveau hook, hook_view, et une fonction de thème.

function courses_view($node, $teaser = FALSE, $page = FALSE) {
  $node= node_prepare($node, $teaser);
  if ($page) {
    $node->content['Informations'] = array(
        '#value' => theme('courses_view', $node),
        '#weight' => 1,
    );
  }
  return $node;
}
à ajouter à courses.module

Notre première tâche est de préparer le node à l'affichage. C'est le rôle de la fonction node_prepare qui va appliquer les filtres d'entrée au corps et fabriquer le résumé (teaser).

Une fois le node préparé, nous vérifions dans quel contexte nous sommes appelé pour n'inclure nos informations supplémentaire que lorsque Drupal a besoin du node en entier (mode page) et pas seulement du résumé (mode teaser).

Si nous sommes bien en mode page, nous pouvons ajouter à l'objet $node des sections $node->content[]. Ces sections seront affichée par Drupal après le titre et le corps du node. Chacune d'entre elles est composée d'un contenu HTML (propriété #value) et d'un ordre d'apparition (propriété #weight).

Pour renseigner #value nous rajoutons une petite finesse : l'utilisation d'une fonction de thème theme('courses_view', $node). Cela consiste à demander à Drupal d'habiller l'objet $node avec le thème courses_view. La fonction en interne va procéder de la manière suivante :

  • Si votre site a un thème, par exemple mon_theme, Drupal vérifie s'il existe une fonction qui s'appelle mon_theme_courses_view. Si tel est le cas, c'est elle qui sera utilisée.
  • Si votre site utilise un theme engine, par exemple phptemplate, Drupal cherche une fonction nommée phptemplate_courses_view.
  • Le cas échéant, Drupal cherche une fonction nommée theme_courses_view. Si elle existe, c'est elle qui sera utilisée.

Une bonne pratique est donc de toujours fournir un thème par défaut en utilisant ce dernier cas de figure. Ainsi, Drupal trouver toujours de quoi habiller notre node tout en laissant à vos utilisateurs la possibilité de changer cet habillage. Là aussi c'est une bonne pratique à garder à l'esprit.

function theme_courses_view($node)
{
  $result = "<h2>Détail du produit</h2>";
  $result.= "<div class='quantite'>Quantité restante :".$node->quantite."</div>";
  $result.= "<div class='seuil'>Seuil avant rachat : ".$node->seuil."</div>";
  return $result;
}
à ajouter à courses.module

Rajouter des actions standard

Vous aurez sans doute remarqué que chaque node disposent, à la fin du texte (teaser ou page), d'une série de liens qui offrent des actions qui lui sont propres. Ces liens sont définis grâce à l'implémentation de hook_view. Nous allons donc utiliser cette fonctionnalité pour ajouter à chaque node un lien vers notre future liste de courses.

.
function courses_link($type, $node = NULL, $teaser = FALSE) {
  $links = array();
  if ($type == 'node' && $node->type='produit') {
      $links['liste_des_courses'] = array(
        'title'=>t('Liste des courses'),
        'href'=> "courses/liste",
        'attributes'=>array('title' => t('Afficher la liste complète des courses.')));
  }
  return $links;
}
à ajouter à courses.module

Attention cependant, contrairement à tous les hooks que nous avons étudié jusqu'à maintenant, celui là fonctionne de manière globale. C'est à dire qu'il n'est pas appelé spécifiquement pour un node de type produit. Et il est même utilisé ajouter des liens à d'autres éléments de Drupal, comme les commentaires. Il faut donc vérifier avant d'ajouter un lien qu'il s'agit bien d'un node, et que le type du node est bien produit. Ensuite tout passe par un nouveau tableau associatif pouvant contenir autant de liens que désiré. La syntaxe est assez simple pour être comprise en première lecture.

Maintenant que nous avons notre lien vers la liste des courses, il ne nous reste plus qu'à la mettre en oeuvre.

Liste de courses

Avant de rentrer dans le vif du sujet, arrêtons nous un peu sur la manière dont Drupal gère les URL. Ces dernières sont de la forme http://mon_site_drupal?q=chemin1/chemin2 ou http://mon_site_drupal/chemin1/chemin2 selon que le mode "URL Simplifié" est actif ou non. La chaîne chemin1/chemin2, appelée path, est associée par Drupal à une fonction, appelée callback. Lorsqu'il est exécutée, ce callback est renvoyé à Drupal du code HTML qu'il va l'insérer dans le corps de la page. Cette association entre un path et une fonction callback est appelée un menu.

Chaque module a la possibilité de définir ses propres menus en implémentant hook_menu. C'est ce que nous allons utiliser pour afficher notre liste de courses :

function courses_menu($may_cache) {
  $items= array ();
  if ($may_cache) { }
  else {
    $items[]= array (
      'title' => t("Liste de courses"),
      'path' => 'courses/liste',
      'callback' => 'courses_callback_liste',
      'type' => MENU_NORMAL_ITEM,
      'access' => TRUE,
    );
  }
  return $items;
}

function courses_callback_liste() {
drupal_set_title(t('Liste des courses'));
  return "Je suis une <b>liste</b>";
}
à ajouter à courses.module

Une fois le code ajouté allez à l'URL http://mon_site_drupal/?q=courses/liste_items et voyez le résultat. Notre texte s'affiche tranquillement au milieu de la page et le titre de la fenêtre du navigateur est bien Liste des courses.

courses_menu renvoie à Drupal un tableau dont chaque entrée est un tableau associatif définissant un menu. Ce menu associe comme nous l'avons vu plus haut un path à une fonction callback. Le choix de mettre le nom du module, courses comme premier élément du path est une convention permettant d'avoir des URL de la forme mon_module/mon_action.

A ce menu nous avons donné un titre (paramètre title) et vous aurez sans doute constaté qu'un lien avec ce texte est apparu en même temps que la liste dans votre bloc de navigation. Cette magie là, nous la devons au type de menu (paramètre type) positionné à MENU_NORMAL_ITEM. Cela indique à Drupal que ce doit être un menu visible.

Enfin Le dernier paramètre access positionné à TRUE permet à n'importe quel utilisateur, anonyme ou pas, d'accéder à ce menu.

La fonction de callback commence par changer le titre de la page et poursuit par la construction du contenu HTML du corps qui sera renvoyé à Drupal.

Il ne nous reste maintenant plus qu'à mettre un véritable corps à notre callback pour obtenir enfin notre liste de courses

function courses_callback_liste() {
  $path= drupal_get_path('module', 'course');
  drupal_add_css($path . '/default.css');
  drupal_set_title(t('Liste des Courses'));
$cursor = db_query("
    select n.*,i.* from node n
      inner join node_produit i on n.nid=i.nid
    where n.type='produit'
    order by n.title"
);

  $output= "<table class='liste_produits'>
  <tr><th>Produit</th><th>Seuil</th><th>Quantité</th><th>Action</th></tr>
  "
;
  while ($node=db_fetch_object($cursor)) {
    $output .= "<tr";
    if ($node->restant < $node->minimum) {
      $output .= " class='manquant'";
    }
    $output .= "><td>".$node->title."</td><td>".$node->seuil."</td><td>$node->quantite</td>";
    $output .= "<td>";
    if ($node->quantite>0) {
      $output .= l("Diminuer","courses/diminuer/".$node->nid);
      $output .= " | ";
    }
    $output .= l("Modifier","node/".$node->nid."/edit");
    $output .= "</td>";
    $output .= "</tr>";
  }
  $output.="</table>";
  return $output;
}
corps de la fonction de callback

La fonction commence par deux lignes dont l'objectif est de demander à Drupal de prendre la feuille de style default.css qui se trouve dans le dossier du module, et de l'ajouter à la page finale. Pour plus d'informations sur cette méthode, reportez vous à ce tutoriel. Cette feuille de style va juste nous servir à mettre en évidence les lignes de produits qui doivent être rachetés.

TR.manquant TD {
  color: red;
}
Fichier default.css à mettre dans le dossier du module

La requête SQL qui suit nous permet d'itérer sur la liste des nodes de type produit en y ajoutant les informations de la table node_produit. Nous aurions pu seulement récupérer ici le nid et utiliser la fonction node_load mais cette méthode est beaucoup plus rapide. Ceci fait, le reste n'est plus qu'un travail de mise en forme du contenu de l'objet $node en ligne d'un tableau, qui sert de retour à la fonction de call-back.

La dernière colonne du tableau contient des liens qui représentent les actions par produit : modifier et diminuer. La première est simplement un raccourcis sur l'édition du node, la seconde va nous permettre de diminuer la quantité restant de produit d'un simple clique.

Notez l'utilisation de la fonction Drupal, l(…). Son rôle est de fabriquer un lien HTML à partir d'un texte et d'un chemin. Il est fortement conseillé de toujours utiliser cette fonction pour générer un lien et ainsi laisser Drupal en trouver la meilleur représentation URL.

Voyons maintenant comment câbler notre action diminuer.

Interactivité

L'action diminuer attend que Drupal sache reconnaître un path de la forme couses/diminuer/1234. 1234 représentant le nid de la ligne courante. Pour cela nous allons devoir créer un nouveau menu d'un genre un peu différent du précédent, c'est à dire capable de comprendre un paramètre (le nid) et ne devant pas s'afficher dans le bloc de navigation.

if (is_numeric(arg(2))) {
  $items[]= array (
    'path' => "courses/diminuer/".arg(2),
    'callback' => 'courses_callback_diminuer_quantite',
    'type' => MENU_CALLBACK,
    'callback arguments' => array (arg(2)),
    'access' => TRUE
  );
}
à insérer après le premier menu

Plusieurs points méritent attention. Tout d'abord, la fonction arg(n). C'est une fonction Drupal qui renvoie le morceau du path courant se trouvant à la position n (n débutant à 0). Pour le path courses/retirer/1234, 1234 correspond donc à arg(2).

Pour ne pas surcharger la liste des menus de Drupal, nous n'ajoutons ce nouveau menu que lorsque le troisième argument, arg(2), est une valeur numérique. N'oubliez pas que le code de Drupal est exécuté à chaque requête utilisateur. Du coup, tous les hook_menu de tous les modules actifs sont balayés. Il est donc important d'optimiser au maximum.

Ensuite nous fabriquons un path dynamique à l'aide de cet argument et nous précisons à Drupal de passer cet argument en paramètre de la fonction de callback courses_callback_diminuer_quantite. Enfin, nous utilisons le type MENU_CALLBACK qui va nous permettre de créer un menu qui ne soit pas affiché dans le block de navigation.

Il ne nous reste plus qu'à écrire cette fonction de callback :

function courses_callback_diminuer_quantite($nid) {
  $node=node_load($nid);
  if ($node->quantite>0) {
    $node->quantite--;
  }
  node_save($node);
  return courses_callback_liste();
}

Comme tout menu, la fonction callback doit renvoyer un résultat à afficher, nous poursuivons donc le traitement en appelant la callback qui affiche le menu.

Le traitement effectué par cette fonction de callback consiste à charger dans l'objet $node le node dont le nid est passé en paramètre. Ensuite à décrémenter sa quantité puis à sauver le objet $node. Notez que cela aurais pu être fait en une seule requête SQL, sans passer par Drupal mais cela aurais été moins instructif.

Conclusion

Voilà, nous avons terminé notre premier module qui avec moins de 200 lignes de code, permet de faire tout ce que nous voulions. Il est évidemment possible d'aller beaucoup plus loin, d'ajouter des champs dans la liste pour simplifier l'édition par exemple ou encore utiliser des callback en ajax pour réduire le nombre d'items.

Quoi qu'il en soit, j'espère vous avoir montré à quel point, une fois les notions de base assimilées (hooks, nouveau type de contenu, menus, etc.) il est simple de forger un module qui corresponde parfaitement à un besoin particulier.

Comments

anti-pixel (not verified), le 15 May, 2008 - 19:36

Ca m'a l'air d'être un très bon tuto que je vais tester très bientôt...

Ulhume, le 15 May, 2008 - 20:45

Et bien écoute merci Smiling C'est un des plus long que j'ai eu à réaliser et il est encore incomplet à mon sens. Il lui manque :
- Une vue custom pour un node
- La validation des formulaires, le problème est que dans mon exemple je n'ai rien à valider Wink
- La gestion des droits
- Un panneau d'administration associé

C'est pour cela qu'il est encore en béta, avec cela en plus, ça devrait être complet. Si j'ai oublié quelque chose, ne pas hésiter !!

anti-pixel (not verified), le 16 May, 2008 - 14:59

16h00 je m'ennuie au boulot, et j'ai une heure à tuer, je commence le tuto Glad

anti-pixel (not verified), le 16 May, 2008 - 15:49

45mn plus tard, et bien que cette fin de semaine me laisse que très peu de neurones, je te donne, mon avis...
J'ai suivi (bêtement) le tuto sans trop réfléchir, et j'ai un résultat qui fonctionne relativement (et qui marcherait parfaitement si je m'étais appliqué). Selon moi, ce n'est pas une bonne idée de rajouter tous les élements dont tu parles à la suite mais plutot comme une seconde partie du tuto (l'idée étant de fournir différents niveaux pour aider à la compréhension par échelon). Un autre truc sympa serait de fournir les fichiers de ton module pour les fainéants qui sont fatigués du copier-coller Glad

Et puis sinon (dsl), attention aux fautes Glad

Voila, tuto très utile pour les dev drupal !

Ulhume, le 16 May, 2008 - 16:41

@Antipixel tu parles de fainéants qui auraient du mal à lire ? Wink)) Bon, j'avoue que c'est pas super facile à voir, juste après la conclusion, tu as un petit encadré :
L'ensemble des sources de ce tutoriel est disponible ici.

Là tu as accès à tous les sources sur un serveur subversion.

Sinon, ok pour le tuto "niveau avancé", c'est effectivement une bonne idée.

En tout cas merci pour ton aide.

PS: pour l'orthographe j'atteins là mon seuil d'incompétence. J'appartiens à un bout de génération qui en france est passé dans une faille temporelle d'apprentissage du sujet. l'époque les instit's n'avaient que le mot "éveil" à la bouche... j'ai découvert le concept de "dictées" un peu tard... Si tu as un tuto pour moi à ce sujet je suis preneur ! Smiling

anti-pixel (not verified), le 17 May, 2008 - 14:42

mea culpa, j'avais pas vu le petit encadré pour les sources Glad
lol j'ai pas de tuto pour l'orthographe dsl, je n'y fais pas trop attention dans les messages et les petits posts mais un peu plus sur les gros tutos Wink

Dudesque (not verified), le 4 December, 2008 - 11:22

pour les tutos d'orthographe, ya 2 modules français que je n'ai pas reussi a installer: Bled et Bechrelle, l'installation est ardu mais parait il que ce sont des implementations robuste du core langue_française

bon treve de plaisanteries, je voulais tout simplement remercier artisant numerique pour les tutos sur les modules drupal! Il sont vraiment super didactique et très claire pour un niveau debutant en dev de module drupal!

J'ai commencer avec drupal il y a quelques mois et je dois dire que le site m'a beaucoup aider meme si j'ai encore des gros soucis.
Je regarde ce tuto qui m'interesse et j'y reviens dans les commentaires!

Anonymous (not verified), le 20 July, 2008 - 13:17

Bonjour,
Avant tout merci pour ce tuto. Je me sens un peu moins seul...
J'ai suivi à la lettre vos instructions :
"A ce stade nous pourrions donc activer notre module, mais ne le faites pas tout
de suite, nous avons encore quelque chose à rajouter. ==> OK

j'ai continué :
"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."
==> Dans mon cas, rien n'apparait.

J'ai poursuivi cependant :
"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."
==> rien n'apparait encore une fois

Merci de votre aide afin que je puisse continuer à avancer dans ma longue, difficile mais passionnante aventure de création d'un web !!!

Je souhaite également mettre gallery2 dans drupal. La config s'est bien passée mais au chargement de mon web je ne vois pas d'accès à la gallerie !
Existe-t-il un tuto dans le style du votre sur le sujet ?

Bien à vous
Onurbski

Ulhume, le 20 July, 2008 - 23:03

Quelle version de Drupal utilisez-vous ? Est-ce un apache que vous hébergez ou mutualisé ?

Sinon, une bonne manière de tester d'où cela peu venir est de coller une trace en début de fonction "statistiques_block", quelque chose du genre
error_log("======> ".$op);

Et de regarder dans les traces apache si cela appairait.

onurbski, le 20 July, 2008 - 23:39

J'utilise :
web server : zazoumiwebserver 1.2.8
DB : mysql 5.0.51 community
php version : 5.2.5 cgi-fcgi
Gallery2 : 2.2.5
OS : XP
Browser : Firefox 2.0.0.16

Pour l'instant, je suis en local sur mon PC et je fais des tests avant déploiement sur free.
J'ai pu créer un article sur drupal.
J'ai pu créer des albums sur gallery2.
Le problème est qu'au lancemnt de drupal je vois en première page du web mon article mais je ne vois rien pour avoir accès à gallery2.
J'ai mis gallery2 sous ...DRUPAL\sites\all\gallery2.
Etant tout débutant, je patauge mais persévère. Savez-vous s'il existe des tutos avec exemple pratique ?
Merci
Onurbski

Ulhume, le 21 July, 2008 - 06:41

C'est de la version de DRUPAL dont j'avais besoin Smiling

Pour gallery2, je n'ai vraiement jamais utilisé, mais en toute logique cela doit marcher par node, comme les articles. Donc si vous fait un ?q=admin/content/node est-ce que vous voyez votre galerie dans la liste. Normalement, elle devrait avoir un "type" particulier. Et si oui, pouvez vous l'afficher en cliquant sur la 1iere colonne ?

Sinon, une possibilité classique est une erreur sur le système de fichier, pour cela, allez dans ?q=admin/reports/status et vérifiez que vous n'avez pas d'erreur affichée. Corrigez le cas échéant ?

Sinon pour le tuto sur les blocs, vous avez essayé les traces ?

Ben (not verified), le 13 August, 2008 - 15:47

Pour répondre à ce problème :

**************
"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."
==> Dans mon cas, rien n'apparait.
**************

=> Il faut activer le module créé (?q=admin/build/modules) pour visualiser le bloc

Ulhume, le 13 August, 2008 - 22:50

@Ben il me semble que c'est précisé dans le tuto ?

charly (not verified), le 19 August, 2008 - 20:47

bonjour,

on accede comment a l'url du module ?
http://mon_site_drupal?q=statistiques

car avec cette url cela ne fonctionne pas

J'aimerais developper un module afin de gerer la liste des membres de mon site www.varouler.com
car la liste d'origine n'a pas de mise en page.

J'ai telecharger le module user_liste mais celui ci n'affiche pas les fotos en mini.
Je voudrais donc le modifier.

merci de votre aide
charlyy

Ulhume, le 24 August, 2008 - 08:12

Que veux tu dire par "URL du module" exactement ?

Kyborash (not verified), le 18 November, 2008 - 10:22

Bonjour,

Merci pour le tuto, je commence juste Drupal et je pense que ça va vraiment m'aider Smiling

Par contre, à propos du fait que rien n'apparaisse dans la liste des blocs désactivés, parce que - comme l'a dit Ben - il faut activer le module créé, tu lui as répondu : "@Ben il me semble que c'est précisé dans le tuto ?"

Je me permets de signaler que tu ne l'as pas précisé Glad
Tu as bien dit "A ce stade nous pourrions donc activer notre module, mais ne le faites pas tout de suite, nous avons encore quelque chose à rajouter.", mais ensuite, tu ne dis pas "Vous pouvez maintenant activer votre module pour poursuivre" ou quelque chose du genre...

Donc c'est le coup d'oeil aux commentaires qui m'a permis effectivement d'avoir réponse au problème.

Mais en tout cas, merci pour le tuto Smiling

Kyborash (not verified), le 18 November, 2008 - 10:41

Etant parvenue à bout du tuto, je vais me permettre encore quelques remarques Smiling

- "Ici nous choisissions une case à cocher (checkbox)." => Dans le morceau de code que tu donnes, il s'agit d'un champ texte plutôt, non ?

- "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." => le titre de mon champ n'apparaît pas, même si j'enlève la fonction t(...). Une idée pour une solution ?

- "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é." => le problème étant que dans ton premier bout de code, on ne trouve pas "content' => 'Un bloc vide');" comme tu le dis ensuite mais "'content' => courses_contenu());"
Donc quand on enregistre le bloc, on a une erreur PHP puisque la fonction courses_contenu() n'existe pas encore !

A part ça, tout est très bien (sauf les fautes d'orthographe mais quelqu'un l'a déjà signalé et puis, il n'y en a pas tant que ça Glad), et je vais m'attaquer à la suite Smiling

Merci encore Big grin

Marc (not verified), le 18 November, 2008 - 11:15

Il me semble qu'il manque la balise fermante de la liste

dans le contenu du bloc.
Ce tutoriel est parfait on comprend tout et permet d'avoir un exemple tout de suite pour coder son propre module.
Merci beaucoup.
Marc

Ulhume, le 18 November, 2008 - 12:33

@Kyborash C'est bon, j'ai normalement tout corrigé Smiling En fait j'essaye de recadrer une série de tutos Drupal sur un même sujet (la liste de courses). C'est pour cela qui restait des incohérences liées à l'ancienne version (un bloc de statistiques). Et pour ce qui est de l'orthographe, c'est une longue route pour y arriver mais je persévère Smiling

Merci pour ton aide.

Ulhume, le 18 November, 2008 - 12:34

@Marc yep tu as raison, c'est corrigé, merci Smiling

Kyborash (not verified), le 20 November, 2008 - 10:59

"C'est pour cela qui restait des incohérences liées à l'ancienne version (un bloc de statistiques)." => aaaah, c'est pour ça qu'il y avait le mot statistiques qui traînait un peu partout ! Glad

Et donc à part ça, une idée de pourquoi le titre de mon bloc ne s'affiche pas ?

Kyborash (not verified), le 24 November, 2008 - 14:29

Bon, j'ai fini par trouver toute seule, si j'enlève les accents dans les chaînes passées à la fonction t(...), ça affiche mon texte correctement.
A noter que les accents notés en HTML passent correctement Glad

Ulhume, le 24 November, 2008 - 14:42

@Kyborash je subodore un soucis dans ta configuration. Tu es sure que php_mbstring est installé ?

Kyborash (not verified), le 24 November, 2008 - 15:47

Aucune idée, ce n'est pas moi qui gère ce genre de choses mais je pourrais poser la question Glad

Ulhume, le 24 November, 2008 - 16:59

Hum... Ce n'est pas toi qui gère ta plate-forme de dev ???

Dans tous les cas, tu peux savoir cela dans le tableau de bord de Drupal.

Kyborash (not verified), le 24 November, 2008 - 17:04

Non, c'est mon collègue qui gère la plateforme Glad
Sur Drupal, je me contente de faire de l'intégration graphique et de créer de nouveaux modules.

J'ai vérifié dans le tableau de bord, il n'y a rien qui ressemble à php_mbstring, donc je suppose qu'il n'y est pas Arf

Ulhume, le 4 December, 2008 - 11:42

@Dudesques j'ai déjà installé ces deux modules sur mon bureau physiques mais tout le soucis avec ce genre de développement est leur potentielle incompatibilité avec l'existant. Je suis issu d'une génération sur laquelle l'éducation national expérimentait le remplacement de ces bases arides de la langue française par le beaucoup plus chatoyant "éveil".

Ceci dit, trêve de plaisanteries là aussi, je suis un grand amateur de versions corrigées qui me permettent de m'améliorer, ce que je fais sans cesse depuis que j'écris ici. Cela fonctionne suffisamment pour que je me fasse très peur en lisant les articles que j'ai écris il y a quelques années Wink

En tout cas ravi que les tutoriels soient utiles.

Ulhume, le 4 December, 2008 - 11:43

@Kyborash Désolé, je n'avais pas vu ton commentaire.

Si tu n'as pas ce module PHP, cela explique ton problème avec les accents. Il faut impérativement que tu le fasses installer.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • 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.
  • Lines and paragraphs break automatically.
  • Textual smileys will be replaced with graphical ones.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options

User login
Recent comments