Une nouvelle charte a été élaborée pour le Planet-Libre. Tous les membres sont invités à la consulter et à la respecter.

Nous Suivre

    feed feed feed

En Direct de la Galerie

En Direct du Forum

Les Membres

Participer

Filter les articles :     Articles du jour   -   Articles de la semaine   -   Articles du mois   -   Tous
Gravatar de arNuméral
Drupal.org ni Bazaar ni Subversif, mais sûrement GIT 
  • 8 votes
    vote oui
Par arNuméral, le 15/02/2010 à 20:12.

Ceci étant dit, rien n'est encore fait, mais il semblerait que nous en prenions clairement le chemin.

Pour une part, il est clair que CVS peut être considéré à juste titre comme un gestionnaire de version pour le peu archaïque. Et il ne faut pas s'en étonner, ce système a tout de même plus de 20 ans !

Pour revenir à nos moutons, nous étions beaucoup à nous demander quand drupal.org se déciderait à basculer le dépôt des modules contribution (et du core) sur quelque chose de plus moderne, comme au hasard, subversion. Manque de bol, la grande mode des gestionnaires de version distribués est passée par là, et subversion nous est passé sous le nez au profit de GIT.

Alors qu'est-ce qu'un gestionnaire de version décentralisé ? Disons pour faire (très) simple que la grosse différence avec un centralisé est que sur votre machine, vous ne disposez plus d'une copie de travail, mais d'un dépôt local complet et opérationnel qui sait communiquer son état à des dépôts distants. Cela commence par récupérer d'un dépôt distant (pull), pour y travailler comme d'habitude (commit, update, etc.) et éventuellement, si vous avez les droits, remonter vos modifications sur un dépôt distant (push) qui peut être le même que celui du pull, ou un autre...

La gestion décentralisée de version permet donc à chacun, y compris de petites équipes, de travailler sur les sources d'un projet tout en laissant à d'autre le choix d'intégrer ou pas les modifications au sein du projet central. C'est en quelque sorte la notion de fork généralisé. Ce concept est très intéressant pour permettre à toute une communauté de s'exprimer sur des projets d'envergure comme le noyau Linux, ou dans une moindre mesure, Drupal Core. C'est d'ailleurs Torvald, après une saga autour du gestionnaire de version propriétaire BitKeeper, qui a écrit les premières moutures de GIT. Mais malgré ce que peut dire Torvald, les centralisés ne sont pas pour autant mort, et continuent à répondre à 99.9% des besoins d'équipe de 1 à 10 personnes bossant sur de plus modestes projets. Et le revamping de CVS, subversion, rentre parfaitement dans ce cadre.

Entendons nous bien, je n'ai absolument rien contre ce type de gestionnaire, et encore moins contre GIT que j'envisage d'adopter pour mes propres projets. Mais autant je peux comprendre ce choix dans le cas du core de Drupal, autant pour la myriade de modules contribs, maintenus à grand peine par de petits développeurs dans leur coin (dont je fais parti) qui n'ont pas que cela à foutre de jongler d'un gestionnaire à l'autre. Et si l'on met ceci en perspective du fait que l'adoption massive de Subversion en entreprise est enfin concrétisée, GIT un choix que je trouve un peu... lourd.

D'autant plus lourd que ce produit, comme son concept de base, est très jeune. Cela veut dire apprendre un nouveau mode de fonctionnement pour ceux qui ont déjà eu du mal à passer à SVN, mais cela implique aussi une maturité plus faible sur des plate-formes comme eclipse (qui est aussi la base de Zend Studio). Alors que Subversive arrive enfin à des performances et une stabilité potables, voilà que l'on se retrouve avec EGit, un plugin qui n'en finit pas d'incuber, aux fonctionnalités pour le moins anémiques (pas de vue "dépôt" par exemple), et à la stabilité "hasardeuse" (régulièrement, un menu "team" déserté de toute fonction, le dossier .git visible dans l'arbo, etc). Alors on peut tout gérer en ligne de commande hein, c'est pas moi que ça va gêner, ou alors utiliser un "machin" comme tortoise, mais cela continue de me laisser dans l'idée que ce choix est un tantinet "geek".

Retourner au sommaire
Gravatar de arNuméral
Créer des handlers de menu 
  • 2 votes
    vote oui
Par arNuméral, le 21/12/2009 à 11:07.

Tout drupalien sait ce qu'est un "élément de menu" : un machin qui se colle dans un... menu, et qui apparaît quelque part sur l'interface graphique pour permettre l'affichage d'une page. Ces éléments de menu sont généralement crées à la mano en passant par le backoffice, via la section Construction du site puis Menus. Là on peut ajouter des associations entre un chemin valide et un titre de lien.

Ce "chemin valide" est systématiquement fournit par l'un des modules activé (ex. user/login issu modules/user). Mais alors comment créer ses propres chemins liés à ses propres pages ou actions, sans passer par d'inutile (pour cela) usines à gaz comme Panel ou Views ?

Le but de ce tutoriel est de démystifier ce passage obligé de la vie d'un module.

Les sources

L'ensemble des sources de ce tutoriel est disponible ici. Il s'agit d'un serveur Subversion, donc vous pouvez aussi directement récupérer les sources dans votre dossier site/all/modules par la commande suivante :

gaston$cd /var/www/drupal/site/all/modules
gaston$mkdir tutoriels
gaston$cd tutoriels
gaston$svn co http://www.arnumeral.fr/subversion/public/tutoriels_drupal/tutoriel_menus
...
gaston$ 

Cycle de vie d'un requete

Lorsqu'apache reçoit une requête, par exemple http://mon_site/mon-super-article, il commence par ré-écrire la partie "chemin" de l'URL de sorte à la rendre assimilable par Drupal. C'est le fameux système d'URL simplifiées (clean UEL). L'URL devient alors http://mon_site/index.php?q=mon-super-article, index.php étant le point d'entrée de Drupal.

La partie ?q=mon-super-article est donc une variable $_GET['q'] que va recevoir drupal pour effectuer une action. La première étape pour résoudre ce chemin, est de déterminer si le contenu de $_GET['q'] ne serait pas hasard pas un alias en cherchant une correspondance dans la table url_alias. L'exemple est bien choisi, il en trouvera un qui sera node/666. mon-super-article est donc le chemin, et node/666 le chemin dit "interne", celui que va réellement prendre en charge un module.

Et c'est effectivement la seconde étape de Drupal, trouver quel module est en charge du chemin interne node/666. Plus exactement, et attention il va y avoir risque de confusion de vocabulaire, Drupal va chercher quel "menu" est associé à ce chemin. Pour ne pas entretenir la confusion trop longtemps, un "menu" pour les modules, est une association entre un chemin interne (ici node/666), et un module (ici, le module modules/node). Pour simplifier le discours, j'appellerais cela un menu handler pour le différencier des éléments de menu qui eux, associent un lien à un chemin interne.

Si aucun handler n'est trouvé, Drupal renvoie le fameux 404 fichier non trouve. Si le handler existe bien mais que l'utilisateur qui cherche à le déclencher n'a pas les droits pour cela, il renverra un 401 Access denied. Enfin si le handler existe et que l'utilisateur a les droits, Drupal va transférer le traitement à une des fonctions du module qui a déclaré ce handler.

Cette fonction, appelée callback a plusieurs possibilités. Si le chemin interne correspond à une page (c'est le cas de node/666), la fonction callback va renvoyer, par son return, une chaîne de caractère qui correspondra au coeur de page (région content). Drupal va alors récupérer cette chaîne, la faire passer dans le thème, et ainsi produire la page correspondant au chemin interne. C'est ainsi que l'article http://mon_site/mon-super-article sera affiché.

Autre cas de figure, le chemin interne n'est pas une page, mais une action comme par exemple node/666/delete (suppression d'un article). Dans ce cas, la fonction callback n'a aucune raison de renvoyer une chaîne de caractère. Elle va simplement supprimer l'article, et terminer, sans return par un drupal_goto pour rediriger une un chemin interne d'atterrissage.

Enfin, dernier cas un peu moins usité, le chemin interne doit renvoyer des données, par exemple du code XML pour une procédure AJAX. Dans ce cas le code sera imprimé (print ou echo) directement dans la fonction de callback, qui se terminera par un très brutal exit();, coupant ainsi la chique à Drupal.

Implémentation de hook_menu

Maintenant que le sujet est un peu dégrossi, voyons comment faire déclarer un handler par un module. Pour commencer, il nous faut un construire un module de base (voir ce tutoriel) dans lequel nous allons implémenter un hook_menu. Ce hook permet à Drupal de récupérer l'ensemble des handlers de menu publiés par un module donné et son implémentation ressemble à ceci :

function tutoriel_menus_menu() {
  $items = array ();

  $items['tutoriels/menus/simple'] = array (
    'title' => 'Un menu simple',
    'description' => "Ceci est un menu simple",
    'page callback' => 'tutoriel_menus_simple_menu_callback',
    'file' => 'tutoriel_menus.pages.inc',
    'access arguments' => array (
      'access content'
    )
  );

  return $items;
}
implémentation d'un hook_menu

Chaque handler déclaré par le hook_menu est une entrée pour un tableau qui sera renvoyé en retour de la fonction. La clef utilisée par ce tableau est le chemin interne pris en charge par le handler, et sa valeur, une structure le décrivant. Dans cette structure nous avons des éléments simples à comprendre comme title ou description qui permettrons à Drupal de créer un élément de menu à partir de ce handler. Notez que ces deux chaînes ne doivent pas utiliser la fonction de traduction t(...).

Déclaration de la callback

page callback est l'élément fondamental de ce handler. Il s'agit de la fonction qui va être appelée par Drupal pour effectuer l'action associée au chemin interne. Le paramètre file définit quant à lui dans quel fichier PHP cette fonction se trouve. Cette astuce a permis à Drupal 6 d'améliorer grandement ses performances par rapport à Drupal 5. En effet, file permet de ne charger que les fichiers PHP utiles au traitement d'un chemin, sans charger tout le reste.

Si le paramètre file est omis (ce qui n'est pas conseillé), Drupal cherchera la fonction parmi celles déjà chargée en mémoire. Si ce paramètre est présent, le fichier correspondant sera cherché dans le dossier du module (utilisez le paramètre file path si le fichier se trouve hors de ce dossier). D'une manière général, les callbacks correspondant à des pages sont rangées dans un fichier mon_module.pages.inc. Celles correspondant à l'administration dans mon_module.admin.inc, etc.

Droits associés au handler

Le dernier paramètre, access argument, contient un tableau de permissions nécessaires à l'accès au menu. Ici access content désigne le droit d'accéder au contenu, ce que tout le monde peut généralement faire. Mais par exemple pour limiter aux seuls administrateur, nous aurions pu mettre administer site configuration.

Ce paramètre access argument est en réalité utilisé conjointement avec le paramètre access callback. Mais lorsque ce dernier est omis, Drupal utilise par défaut la valeur user_access. Ainsi vous comprenez que ces deux paramètres correspondent en réalité à un appel à la fonction user_access(array('access content')) qui renvoie vrai si l'utilisateur courant a les droits demandés. A titre d'exemple, vous auriez pu aussi utiliser pour access callback les fonctions is_anonymous_user ou user_is_logged_in, toute deux fournies par le module user. Comme elles ne prennent pas de paramètre, le paramètre access arguments peut dans ce cas être omis.

D'une manière générale, vous pouvez utiliser la fonction qui vous chante pour gérer les droits, pour peu qu'elle renvoie true si l'utilisateur est valide et false dans le cas contraire.

Implémentation de la callback

Il ne nous reste maintenant plus qu'à implémenter la callback, en commençant par créer un fichier tutoriel_menu.pages.inc et y placer une fonction comme celle-ci :

function tutoriel_menus_simple_menu_callback() {
  $output="Coeur de page associé à ce modeste menu";
  return $output;
}
tutoriel_menus.pages.inc - callback générique

Bon, un peu simpliste comme rendu, mais c'est juste pour l'exemple. Vous pouvez mettre dans $output tout ce qui vous chante, de simple messages à des tableaux triables.

Vidange du cache de menus

Il ne nous reste maintenant plus qu'à tester cela. Attention cependant, ce hook n'est, pour des raisons de performances, invoqué qu'à l'activation du module. C'est très bien pour la première fois, mais moins drôle lors des essais suivants. Pour régler ce problème, je vous conseille d'installer le module administration menu qui dans son menu déroulant de droite, dispose d'une action de vidange du cache des menus.

Une fois le module activé, vous devriez voir apparaître dans votre menu Navigation, le nouveau menu Un menu simple. En l'activant, dans le coeur de page, doit apparaître le retour de notre callback.

Des éléments menus arborescents

Il est possible de prolonger l'exemple précédent en créant une cascade de menu. Basiquement, une telle arborescence consiste simplement à créer un menu par niveau de chemin. Dans le chapitre précédent, le chemin était tutoriels/menus/simple. Pour rendre visible les deux niveaux précédents, il nous font donc créer deux menus respectivement pour tutoriels, puis tutoriels/menus.

$items['tutoriels'] = array (
  'title' => 'Les tutoriels',
  'page callback' => 'tutoriel_menus_tutoriels_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'expanded'=>true  
);

$items['tutoriels/menus'] = array (
  'title' => 'Les menus',
  'description' => t("Tutoriel sur les menus"),
  'page callback' => 'tutoriel_menus_tutoriels_menu_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'expanded'=>true  
);
Ajout de deux niveaux de menu

Notez la propriété expanded qui permettent d'auto-déployer les deux niveaux pour que tout soit visible par défaut. Enfin, comme nous utilisons deux nouvelles callback, il faut aussi les ajouter dans tutoriel_menu.pages.inc

function tutoriel_menus_tutoriels_callback() {
  return "Les tutoriels";
}

function tutoriel_menus_tutoriels_menus_callback() {
  return "Les tutoriels des menus";
}
Ajout des deux callback

Il suffit maintenant de reconstruire les menus et d'observer le résultat.

Choix du menu cible

Par défaut, nos éléments de menu sont intégrés par Drupal dans le menu administration si le chemin commence par admin/..., ou dans le menu navigation le cas échéant. Il est cependant possible d'ajouter nos handlers ailleurs en utilisant la propriété menu_name. Pour illustrer cette possibilité, nous allons ajouter aux liens primaires (un menu généralement affiché en haut à droite de chaque page), l'élément de menu A propos de nous :

$items['informations'] = array (
  'title' => 'A propos de nous',
  'page callback' => 'tutoriel_menus_a_propos_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'menu_name' => 'primary-links'
);
Ajout d'un élément de menu dans le menu 'liens primaires'

Comme toujours, il nous faut une nouvelle callback :

function tutoriel_menus_a_propos_callback() {
  return "Bla bla bla...";
}
Callback pour l'élément de menu 'information'

Une fois le cache des menus reconstruit, si vous allez dans l'administration des menus, sur le menu Liens primaires, vous devriez voir apparaître le nouvel élément de menu. Et si votre thème l'affiche par défaut en haut à droite de la page, il y sera directement visible.

L'astuce ici consiste à savoir que le nom interne du menu liens primaires est primary-links. En utilisant ce nom pour la propriété menu_name, nous avons forcé l'ajout de ces menus aux liens primaires. Notez que c'est un ajout totalement dynamique, au sens où les menus ainsi créés disparaîtront d'eux même à la prochaine reconstruction des menus si vous les supprimez de votre hook. En somme une alternative intelligente pour déployer facilement des menus en production sans avoir à se refrapper une configuration manuelle, par exemple pour instancier toutes les sections d'un site sur le menu "liens secondaires".

Les onglets

Une autre manière de gérer les menus arborescents est de les afficher sous la forme d'onglet. Drupal permet en effet de gérer ainsi deux niveaux d'onglets (vous en avez un exemple dans la configuration d'un thème). Ces onglets sont très faciles à mettre en oeuvre pour peu d'en comprendre la logique.

$items['tutoriels/menus/simple/onglet-1'] = array (
  'title' => 'Onglet 1',
  'type' => MENU_DEFAULT_LOCAL_TASK,
  'access arguments' => array (
    'access content'
  )
);

$items['tutoriels/menus/simple/onglet-2'] = array (
  'type' => MENU_LOCAL_TASK,
  'title' => 'Onglet 2',
  'page callback' => 'tutoriel_menus_onglet2_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  )
);

$items['tutoriels/menus/simple/onglet-2/onglet-2-1'] = array (
  'title' => 'Onglet 2.1',
  'access arguments' => array (
    'access content'
  ),
  'type' => MENU_DEFAULT_LOCAL_TASK
);

$items['tutoriels/menus/simple/onglet-2/onglet-2-2'] = array (
  'title' => 'Onglet 2.2',
  'page callback' => 'tutoriel_menus_onglet22_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'type' => MENU_LOCAL_TASK
);
Ajout d'onglets

Comme vous le voyez, nous sommes ici proche des menus arborescents. La nouveauté est tout d'abord l'utilisation du paramètre type indiquant à Drupal que nous définissons ici des onglets (appelés Tasks, ou tâches). MENU_LOCAL_TASK marque un onglet normal et MENU_DEFAULT_LOCAL_TASK marque l'onglet par défaut pour un niveau. Notez l'absence de callback pour les handlers marqués MENU_DEFAULT_LOCAL_TASK. En effet, la règle est que chaque niveau d'onglet doit disposer d'un MENU_DEFAULT_LOCAL_TASK, et que ce handler est automatiquement au chemin interne de son handler parent. En d'autres termes, si vous cliquez sur Onglet 1, c'est le chemin de notre menu simple qui s'affiche et son handler qui est utilisé. De même en cliquant sur Onglet 2.1, c'est le handler et donc le chemin et la callback de l'onglet 2 qui est utilisé. Du coup, nous n'avons que deux callback à ajouter

function tutoriel_menus_onglet2_callback() {
  return "Contenu de l'onglet 2";
}

function tutoriel_menus_onglet22_callback() {
  return "Contenu de l'onglet 22";
}
Callbacks pour les onglets

Les handlers \"cachés\"

Comme nous l'avons vu plus haut, nous fabriquons dans notre module des handlers de menu, et non des éléments de menus. Si jusqu'à maintenant nous avions une création automatique d'éléments de menu pour chacun de nos handlers, c'est soit que nous ométions le paramétrage type, qui a pour valeur par défaut MENU_NORMAL_ITEM (comprendre "un handler pour lequel drupal doit créer un élément de menu"), ou que nous voulions afficher des onglets avec MENU_LOCAL_TASK et MENU_DEFAULT_LOCAL_TASK.

Maintenant, dans de nombreux cas nos modules n'ont aucun besoin d'éléments de menu mais juste une URL associée à une page et/ou une action. C'est par exemple le cas si nous voulons créer une action d'ajout, ou dans l'exemple qui suit, un simple Hello World.

$items['tutoriels/menus/hello'] = array (
  'page callback' => 'tutoriel_menus_hello_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'type'=>MENU_CALLBACK
);
Ajout d'un handler sans élément de menu

Et comme toujours, nous ajoutons la fonction callback associée

function tutoriel_menus_hello_callback() {
  return "Hello World";
}
Callback du handler sans élément de menu

Une fois le cache de menu reconstruit, vous constaterez que cette fois, aucun élément de menu n'a été rajouté par Drupal. Pour utiliser ce handler, il nous faut directement taper son URL (ou chemin interne) directement dans la zone d'adresse du navigateur, soit http://mon_site/tutoriels/menus/hello.

Les seules différences avec les handlers que nous avons créé jusqu'à maintenant sont que nous n'avons fournit à Drupal ni titre, ni description, et que nous avons en revanche spécifié MENU_CALLBACK comme paramètre type. Ce type permet juste de dire à Drupal qu'il n'est pas utile de cherche à créer un élément de menu.

Handlers paramétrés

Ce type de handler sans élément de menu est très utile pour créer des actions comme "ajouter", "supprimer", etc. Encore faut il pouvoir fournir des paramètres à ce menu de sorte à pouvoir indiquer, dans l'URL, la référence de l'objet à détruire.

Depuis la version 6, Drupal dispose d'un système très bien fait pour passer des paramètres au handler, basé sur le caractère %. Ainsi si nous ajoutons le menu suivant :

$items['tutoriels/menus/hello1/%'] = array (
  'page callback' => 'tutoriel_menus_hello1_callback',
  'page arguments' => array(3),
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'type'=>MENU_CALLBACK
);  
Ajout d'un handler paramétré

Et que nous ajoutons la callback suivante :

function tutoriel_menus_hello1_callback($message) {
  return "Hello $message";
}
Ajout d'une callback paramétrée

Après reconstruction du menu, nous constatons que l'URL http://mon_site/tutoriels/menus/hello1/gaston provoque l'affichage du message Hello gaston. Pour comprendre ce qui se passe, retournons sur la déclaration de notre handler.

D'abord, nous avons dans le chemin interne un symbole % qui indique à Drupal que cet élément de chemin est un paramètre et peut donc prendre n'importe quelle valeur (ici gaston). Ensuite nous avons un nouveau paramètre page arguments contenant un tableau. Le contenu de ce tableau sera transmis à la callback. Si un de ses éléments est un chiffre, il sera préalablement par l'élément de chemin de rang correspondant (qui commence à Innocent. Si l'élément du tableau n'est pas un chiffre, il sera transmis tel-quel à la fonction.

Dans le cas de notre chemin, nous demandons à Drupal de placer en seul paramètre de la callback l'élément de chemin de rang 3 (c'est à dire le 4ième). C'est pour cela que notre callback dispose d'un paramètre $message qui recevra cette valeur.

Il est possible d'avoir plusieurs paramètres à la callback dont l'ordre et le type sera spécifié par page arguments. Mais plus intéressant encore, il est aussi possible de passer en paramètre des objets préchargés par Drupal. Pour tester cela, ajoutons encore un nouvel handler et sa callback :

  $items['tutoriels/menus/hello2/%user'] = array (
    'page callback' => 'tutoriel_menus_hello2_callback',
    'page arguments' => array(3),
    'file' => 'tutoriel_menus.pages.inc',
    'access arguments' => array (
      'access content'
    ),
    'type'=>MENU_CALLBACK
  );  

  // callback associée  à mettre dans tutoriel_menus.pages.inc
function tutoriel_menus_hello2_callback($user) {
  return "Hello {$user->name}";
}
Ajout d'un handler paramétré avec objet

Reconstruisons le cache des menus et lançons l'URL http://mon_site/tutoriels/menus/hello2/1. Vous devriez alors voir apparaître un Hello administrateur (ou n'importe quel nom que vous aurez utilisé comme administrateur de votre site).

L'astuce ici tient à l'utilisation non plus du simple %, mais de %user. Ce dernier indique à Drupal de "charger en mémoire l'utilisateur aillant pour ID l'élément de chemin donné dans l'URL". C'est ainsi que notre callback hérite d'un objet $user chargé à partir de l'ID 1 (celui de l'administrateur).

Il est intéressant de comprendre comment le "magie" fonctionne ici. Lorsque Drupal rencontre un paramètre de handler de la forme %objet, il va chercher une fonction pré-existante de la forme objet_load. S'il la trouve (ici c'est le cas, il s'agit de user_load), il lui passe en paramètre l'élément de chemin (ici 1) correspondant. Les fonctions objet_load renvoient toujours l'objet chargé à partir du paramètre (ici $user=user_load(1)), et c'est cet objet qui est utilisé comme paramètre à la callback.

En standard, Drupal dispose de plusieurs fonctions de type objet_load. Citons par exemple node_load qui par un %node permet de passer un contenu en paramètre d'une callback.

Mais vous pouvez aussi créer la votre. En effet, si vous utilisez un élément de chemin %mon_objet comme ceci

  $items['tutoriels/menus/hello3/%mon_objet'] = array (
    'page callback' => 'tutoriel_menus_hello3_callback',
    'page arguments' => array(3),
    'file' => 'tutoriel_menus.pages.inc',
    'access arguments' => array (
      'access content'
    ),
    'type'=>MENU_CALLBACK
  );  

  // callback associée  à mettre dans tutoriel_menus.pages.inc
function tutoriel_menus_hello3_callback($mon_objet) {
  return "Hello {$mon_objet['name']}";
}
Utilisation d'un chargeur custom

Vous devez en outre ajouter une fonction de type object_load pour que Drupal puisse faire correspondre le paramètre de rang 3, à l'argument %mon_objet :
function mon_objet_load($id) {
  return array('name'=>"Objet[$id]");
}
Chargeur custom

Vous n'avez maintenant plus qu'à reconstruire les menus et tester http://mon_site/tutoriels/menus/hello3/12 pour voir apparaître "Hello Objet[12]".

Conclusion

Voilà, fin du "petit" tour d'horizon sur les menus. Comme vous l'avez vu, le sujet est pour le peu dense, à la mesure de la richesse du sujet. Ceci étant dit, nous n'avons ici qu'abordé l'essentiel, il reste encore beaucoup de choses à découvrir en explorant l'API des menus.

Retourner au sommaire
Gravatar de arNuméral
Créer des handlers de menu 
  • 0 vote
    vote oui
Par arNuméral, le 21/12/2009 à 11:07.

Tout drupalien sait ce qu'est un "élément de menu" : un machin qui se colle dans un... menu, et qui apparaît quelque part sur l'interface graphique pour permettre l'affichage d'une page. Ces éléments de menu sont généralement crées à la mano en passant par le backoffice, via la section Construction du site puis Menus. Là on peut ajouter des associations entre un chemin valide et un titre de lien.

Ce "chemin valide" est systématiquement fournit par l'un des modules activé (ex. user/login issu modules/user). Mais alors comment créer ses propres chemins liés à ses propres pages ou actions, sans passer par d'inutile (pour cela) usines à gaz comme Panel ou Views ?

Le but de ce tutoriel est de démystifier ce passage obligé de la vie d'un module.

Les sources

L'ensemble des sources de ce tutoriel est disponible ici. Il s'agit d'un serveur Subversion, donc vous pouvez aussi directement récupérer les sources dans votre dossier site/all/modules par la commande suivante :

gaston$cd /var/www/drupal/site/all/modules
gaston$mkdir tutoriels
gaston$cd tutoriels
gaston$svn co http://www.arnumeral.fr/subversion/public/tutoriels_drupal/tutoriel_menus
...
gaston$ 

Cycle de vie d'un requete

Lorsqu'apache reçoit une requête, par exemple http://mon_site/mon-super-article, il commence par ré-écrire la partie "chemin" de l'URL de sorte à la rendre assimilable par Drupal. C'est le fameux système d'URL simplifiées (clean UEL). L'URL devient alors http://mon_site/index.php?q=mon-super-article, index.php étant le point d'entrée de Drupal.

La partie ?q=mon-super-article est donc une variable $_GET['q'] que va recevoir drupal pour effectuer une action. La première étape pour résoudre ce chemin, est de déterminer si le contenu de $_GET['q'] ne serait pas hasard pas un alias en cherchant une correspondance dans la table url_alias. L'exemple est bien choisi, il en trouvera un qui sera node/666. mon-super-article est donc le chemin, et node/666 le chemin dit "interne", celui que va réellement prendre en charge un module.

Et c'est effectivement la seconde étape de Drupal, trouver quel module est en charge du chemin interne node/666. Plus exactement, et attention il va y avoir risque de confusion de vocabulaire, Drupal va chercher quel "menu" est associé à ce chemin. Pour ne pas entretenir la confusion trop longtemps, un "menu" pour les modules, est une association entre un chemin interne (ici node/666), et un module (ici, le module modules/node). Pour simplifier le discours, j'appellerais cela un menu handler pour le différencier des éléments de menu qui eux, associent un lien à un chemin interne.

Si aucun handler n'est trouvé, Drupal renvoie le fameux 404 fichier non trouve. Si le handler existe bien mais que l'utilisateur qui cherche à le déclencher n'a pas les droits pour cela, il renverra un 401 Access denied. Enfin si le handler existe et que l'utilisateur a les droits, Drupal va transférer le traitement à une des fonctions du module qui a déclaré ce handler.

Cette fonction, appelée callback a plusieurs possibilités. Si le chemin interne correspond à une page (c'est le cas de node/666), la fonction callback va renvoyer, par son return, une chaîne de caractère qui correspondra au coeur de page (région content). Drupal va alors récupérer cette chaîne, la faire passer dans le thème, et ainsi produire la page correspondant au chemin interne. C'est ainsi que l'article http://mon_site/mon-super-article sera affiché.

Autre cas de figure, le chemin interne n'est pas une page, mais une action comme par exemple node/666/delete (suppression d'un article). Dans ce cas, la fonction callback n'a aucune raison de renvoyer une chaîne de caractère. Elle va simplement supprimer l'article, et terminer, sans return par un drupal_goto pour rediriger une un chemin interne d'atterrissage.

Enfin, dernier cas un peu moins usité, le chemin interne doit renvoyer des données, par exemple du code XML pour une procédure AJAX. Dans ce cas le code sera imprimé (print ou echo) directement dans la fonction de callback, qui se terminera par un très brutal exit();, coupant ainsi la chique à Drupal.

Implémentation de hook_menu

Maintenant que le sujet est un peu dégrossi, voyons comment faire déclarer un handler par un module. Pour commencer, il nous faut un construire un module de base (voir ce tutoriel) dans lequel nous allons implémenter un hook_menu. Ce hook permet à Drupal de récupérer l'ensemble des handlers de menu publiés par un module donné et son implémentation ressemble à ceci :

function tutoriel_menus_menu() {
  $items = array ();

  $items['tutoriels/menus/simple'] = array (
    'title' => 'Un menu simple',
    'description' => "Ceci est un menu simple",
    'page callback' => 'tutoriel_menus_simple_menu_callback',
    'file' => 'tutoriel_menus.pages.inc',
    'access arguments' => array (
      'access content'
    )
  );

  return $items;
}
implémentation d'un hook_menu

Chaque handler déclaré par le hook_menu est une entrée pour un tableau qui sera renvoyé en retour de la fonction. La clef utilisée par ce tableau est le chemin interne pris en charge par le handler, et sa valeur, une structure le décrivant. Dans cette structure nous avons des éléments simples à comprendre comme title ou description qui permettrons à Drupal de créer un élément de menu à partir de ce handler. Notez que ces deux chaînes ne doivent pas utiliser la fonction de traduction t(...).

Déclaration de la callback

page callback est l'élément fondamental de ce handler. Il s'agit de la fonction qui va être appelée par Drupal pour effectuer l'action associée au chemin interne. Le paramètre file définit quant à lui dans quel fichier PHP cette fonction se trouve. Cette astuce a permis à Drupal 6 d'améliorer grandement ses performances par rapport à Drupal 5. En effet, file permet de ne charger que les fichiers PHP utiles au traitement d'un chemin, sans charger tout le reste.

Si le paramètre file est omis (ce qui n'est pas conseillé), Drupal cherchera la fonction parmi celles déjà chargée en mémoire. Si ce paramètre est présent, le fichier correspondant sera cherché dans le dossier du module (utilisez le paramètre file path si le fichier se trouve hors de ce dossier). D'une manière général, les callbacks correspondant à des pages sont rangées dans un fichier mon_module.pages.inc. Celles correspondant à l'administration dans mon_module.admin.inc, etc.

Droits associés au handler

Le dernier paramètre, access argument, contient un tableau de permissions nécessaires à l'accès au menu. Ici access content désigne le droit d'accéder au contenu, ce que tout le monde peut généralement faire. Mais par exemple pour limiter aux seuls administrateur, nous aurions pu mettre administer site configuration.

Ce paramètre access argument est en réalité utilisé conjointement avec le paramètre access callback. Mais lorsque ce dernier est omis, Drupal utilise par défaut la valeur user_access. Ainsi vous comprenez que ces deux paramètres correspondent en réalité à un appel à la fonction user_access(array('access content')) qui renvoie vrai si l'utilisateur courant a les droits demandés. A titre d'exemple, vous auriez pu aussi utiliser pour access callback les fonctions is_anonymous_user ou user_is_logged_in, toute deux fournies par le module user. Comme elles ne prennent pas de paramètre, le paramètre access arguments peut dans ce cas être omis.

D'une manière générale, vous pouvez utiliser la fonction qui vous chante pour gérer les droits, pour peu qu'elle renvoie true si l'utilisateur est valide et false dans le cas contraire.

Implémentation de la callback

Il ne nous reste maintenant plus qu'à implémenter la callback, en commençant par créer un fichier tutoriel_menu.pages.inc et y placer une fonction comme celle-ci :

function tutoriel_menus_simple_menu_callback() {
  $output="Coeur de page associé à ce modeste menu";
  return $output;
}
tutoriel_menus.pages.inc - callback générique

Bon, un peu simpliste comme rendu, mais c'est juste pour l'exemple. Vous pouvez mettre dans $output tout ce qui vous chante, de simple messages à des tableaux triables.

Vidange du cache de menus

Il ne nous reste maintenant plus qu'à tester cela. Attention cependant, ce hook n'est, pour des raisons de performances, invoqué qu'à l'activation du module. C'est très bien pour la première fois, mais moins drôle lors des essais suivants. Pour régler ce problème, je vous conseille d'installer le module administration menu qui dans son menu déroulant de droite, dispose d'une action de vidange du cache des menus.

Une fois le module activé, vous devriez voir apparaître dans votre menu Navigation, le nouveau menu Un menu simple. En l'activant, dans le coeur de page, doit apparaître le retour de notre callback.

Des éléments menus arborescents

Il est possible de prolonger l'exemple précédent en créant une cascade de menu. Basiquement, une telle arborescence consiste simplement à créer un menu par niveau de chemin. Dans le chapitre précédent, le chemin était tutoriels/menus/simple. Pour rendre visible les deux niveaux précédents, il nous font donc créer deux menus respectivement pour tutoriels, puis tutoriels/menus.

$items['tutoriels'] = array (
  'title' => 'Les tutoriels',
  'page callback' => 'tutoriel_menus_tutoriels_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'expanded'=>true  
);

$items['tutoriels/menus'] = array (
  'title' => 'Les menus',
  'description' => t("Tutoriel sur les menus"),
  'page callback' => 'tutoriel_menus_tutoriels_menu_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'expanded'=>true  
);
Ajout de deux niveaux de menu

Notez la propriété expanded qui permettent d'auto-déployer les deux niveaux pour que tout soit visible par défaut. Enfin, comme nous utilisons deux nouvelles callback, il faut aussi les ajouter dans tutoriel_menu.pages.inc

function tutoriel_menus_tutoriels_callback() {
  return "Les tutoriels";
}

function tutoriel_menus_tutoriels_menus_callback() {
  return "Les tutoriels des menus";
}
Ajout des deux callback

Il suffit maintenant de reconstruire les menus et d'observer le résultat.

Choix du menu cible

Par défaut, nos éléments de menu sont intégrés par Drupal dans le menu administration si le chemin commence par admin/..., ou dans le menu navigation le cas échéant. Il est cependant possible d'ajouter nos handlers ailleurs en utilisant la propriété menu_name. Pour illustrer cette possibilité, nous allons ajouter aux liens primaires (un menu généralement affiché en haut à droite de chaque page), l'élément de menu A propos de nous :

$items['informations'] = array (
  'title' => 'A propos de nous',
  'page callback' => 'tutoriel_menus_a_propos_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'menu_name' => 'primary-links'
);
Ajout d'un élément de menu dans le menu 'liens primaires'

Comme toujours, il nous faut une nouvelle callback :

function tutoriel_menus_a_propos_callback() {
  return "Bla bla bla...";
}
Callback pour l'élément de menu 'information'

Une fois le cache des menus reconstruit, si vous allez dans l'administration des menus, sur le menu Liens primaires, vous devriez voir apparaître le nouvel élément de menu. Et si votre thème l'affiche par défaut en haut à droite de la page, il y sera directement visible.

L'astuce ici consiste à savoir que le nom interne du menu liens primaires est primary-links. En utilisant ce nom pour la propriété menu_name, nous avons forcé l'ajout de ces menus aux liens primaires. Notez que c'est un ajout totalement dynamique, au sens où les menus ainsi créés disparaîtront d'eux même à la prochaine reconstruction des menus si vous les supprimez de votre hook. En somme une alternative intelligente pour déployer facilement des menus en production sans avoir à se refrapper une configuration manuelle, par exemple pour instancier toutes les sections d'un site sur le menu "liens secondaires".

Les onglets

Une autre manière de gérer les menus arborescents est de les afficher sous la forme d'onglet. Drupal permet en effet de gérer ainsi deux niveaux d'onglets (vous en avez un exemple dans la configuration d'un thème). Ces onglets sont très faciles à mettre en oeuvre pour peu d'en comprendre la logique.

$items['tutoriels/menus/simple/onglet-1'] = array (
  'title' => 'Onglet 1',
  'type' => MENU_DEFAULT_LOCAL_TASK,
  'access arguments' => array (
    'access content'
  )
);

$items['tutoriels/menus/simple/onglet-2'] = array (
  'type' => MENU_LOCAL_TASK,
  'title' => 'Onglet 2',
  'page callback' => 'tutoriel_menus_onglet2_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  )
);

$items['tutoriels/menus/simple/onglet-2/onglet-2-1'] = array (
  'title' => 'Onglet 2.1',
  'access arguments' => array (
    'access content'
  ),
  'type' => MENU_DEFAULT_LOCAL_TASK
);

$items['tutoriels/menus/simple/onglet-2/onglet-2-2'] = array (
  'title' => 'Onglet 2.2',
  'page callback' => 'tutoriel_menus_onglet22_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'type' => MENU_LOCAL_TASK
);
Ajout d'onglets

Comme vous le voyez, nous sommes ici proche des menus arborescents. La nouveauté est tout d'abord l'utilisation du paramètre type indiquant à Drupal que nous définissons ici des onglets (appelés Tasks, ou tâches). MENU_LOCAL_TASK marque un onglet normal et MENU_DEFAULT_LOCAL_TASK marque l'onglet par défaut pour un niveau. Notez l'absence de callback pour les handlers marqués MENU_DEFAULT_LOCAL_TASK. En effet, la règle est que chaque niveau d'onglet doit disposer d'un MENU_DEFAULT_LOCAL_TASK, et que ce handler est automatiquement au chemin interne de son handler parent. En d'autres termes, si vous cliquez sur Onglet 1, c'est le chemin de notre menu simple qui s'affiche et son handler qui est utilisé. De même en cliquant sur Onglet 2.1, c'est le handler et donc le chemin et la callback de l'onglet 2 qui est utilisé. Du coup, nous n'avons que deux callback à ajouter

function tutoriel_menus_onglet2_callback() {
  return "Contenu de l'onglet 2";
}

function tutoriel_menus_onglet22_callback() {
  return "Contenu de l'onglet 22";
}
Callbacks pour les onglets

Les handlers \"cachés\"

Comme nous l'avons vu plus haut, nous fabriquons dans notre module des handlers de menu, et non des éléments de menus. Si jusqu'à maintenant nous avions une création automatique d'éléments de menu pour chacun de nos handlers, c'est soit que nous ométions le paramétrage type, qui a pour valeur par défaut MENU_NORMAL_ITEM (comprendre "un handler pour lequel drupal doit créer un élément de menu"), ou que nous voulions afficher des onglets avec MENU_LOCAL_TASK et MENU_DEFAULT_LOCAL_TASK.

Maintenant, dans de nombreux cas nos modules n'ont aucun besoin d'éléments de menu mais juste une URL associée à une page et/ou une action. C'est par exemple le cas si nous voulons créer une action d'ajout, ou dans l'exemple qui suit, un simple Hello World.

$items['tutoriels/menus/hello'] = array (
  'page callback' => 'tutoriel_menus_hello_callback',
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'type'=>MENU_CALLBACK
);
Ajout d'un handler sans élément de menu

Et comme toujours, nous ajoutons la fonction callback associée

function tutoriel_menus_hello_callback() {
  return "Hello World";
}
Callback du handler sans élément de menu

Une fois le cache de menu reconstruit, vous constaterez que cette fois, aucun élément de menu n'a été rajouté par Drupal. Pour utiliser ce handler, il nous faut directement taper son URL (ou chemin interne) directement dans la zone d'adresse du navigateur, soit http://mon_site/tutoriels/menus/hello.

Les seules différences avec les handlers que nous avons créé jusqu'à maintenant sont que nous n'avons fournit à Drupal ni titre, ni description, et que nous avons en revanche spécifié MENU_CALLBACK comme paramètre type. Ce type permet juste de dire à Drupal qu'il n'est pas utile de cherche à créer un élément de menu.

Handlers paramétrés

Ce type de handler sans élément de menu est très utile pour créer des actions comme "ajouter", "supprimer", etc. Encore faut il pouvoir fournir des paramètres à ce menu de sorte à pouvoir indiquer, dans l'URL, la référence de l'objet à détruire.

Depuis la version 6, Drupal dispose d'un système très bien fait pour passer des paramètres au handler, basé sur le caractère %. Ainsi si nous ajoutons le menu suivant :

$items['tutoriels/menus/hello1/%'] = array (
  'page callback' => 'tutoriel_menus_hello1_callback',
  'page arguments' => array(3),
  'file' => 'tutoriel_menus.pages.inc',
  'access arguments' => array (
    'access content'
  ),
  'type'=>MENU_CALLBACK
);  
Ajout d'un handler paramétré

Et que nous ajoutons la callback suivante :

function tutoriel_menus_hello1_callback($message) {
  return "Hello $message";
}
Ajout d'une callback paramétrée

Après reconstruction du menu, nous constatons que l'URL http://mon_site/tutoriels/menus/hello1/gaston provoque l'affichage du message Hello gaston. Pour comprendre ce qui se passe, retournons sur la déclaration de notre handler.

D'abord, nous avons dans le chemin interne un symbole % qui indique à Drupal que cet élément de chemin est un paramètre et peut donc prendre n'importe quelle valeur (ici gaston). Ensuite nous avons un nouveau paramètre page arguments contenant un tableau. Le contenu de ce tableau sera transmis à la callback. Si un de ses éléments est un chiffre, il sera préalablement par l'élément de chemin de rang correspondant (qui commence à Innocent. Si l'élément du tableau n'est pas un chiffre, il sera transmis tel-quel à la fonction.

Dans le cas de notre chemin, nous demandons à Drupal de placer en seul paramètre de la callback l'élément de chemin de rang 3 (c'est à dire le 4ième). C'est pour cela que notre callback dispose d'un paramètre $message qui recevra cette valeur.

Il est possible d'avoir plusieurs paramètres à la callback dont l'ordre et le type sera spécifié par page arguments. Mais plus intéressant encore, il est aussi possible de passer en paramètre des objets préchargés par Drupal. Pour tester cela, ajoutons encore un nouvel handler et sa callback :

  $items['tutoriels/menus/hello2/%user'] = array (
    'page callback' => 'tutoriel_menus_hello2_callback',
    'page arguments' => array(3),
    'file' => 'tutoriel_menus.pages.inc',
    'access arguments' => array (
      'access content'
    ),
    'type'=>MENU_CALLBACK
  );  

  // callback associée  à mettre dans tutoriel_menus.pages.inc
function tutoriel_menus_hello2_callback($user) {
  return "Hello {$user->name}";
}
Ajout d'un handler paramétré avec objet

Reconstruisons le cache des menus et lançons l'URL http://mon_site/tutoriels/menus/hello2/1. Vous devriez alors voir apparaître un Hello administrateur (ou n'importe quel nom que vous aurez utilisé comme administrateur de votre site).

L'astuce ici tient à l'utilisation non plus du simple %, mais de %user. Ce dernier indique à Drupal de "charger en mémoire l'utilisateur aillant pour ID l'élément de chemin donné dans l'URL". C'est ainsi que notre callback hérite d'un objet $user chargé à partir de l'ID 1 (celui de l'administrateur).

Il est intéressant de comprendre comment le "magie" fonctionne ici. Lorsque Drupal rencontre un paramètre de handler de la forme %objet, il va chercher une fonction pré-existante de la forme objet_load. S'il la trouve (ici c'est le cas, il s'agit de user_load), il lui passe en paramètre l'élément de chemin (ici 1) correspondant. Les fonctions objet_load renvoient toujours l'objet chargé à partir du paramètre (ici $user=user_load(1)), et c'est cet objet qui est utilisé comme paramètre à la callback.

En standard, Drupal dispose de plusieurs fonctions de type objet_load. Citons par exemple node_load qui par un %node permet de passer un contenu en paramètre d'une callback.

Mais vous pouvez aussi créer la votre. En effet, si vous utilisez un élément de chemin %mon_objet comme ceci

  $items['tutoriels/menus/hello3/%mon_objet'] = array (
    'page callback' => 'tutoriel_menus_hello3_callback',
    'page arguments' => array(3),
    'file' => 'tutoriel_menus.pages.inc',
    'access arguments' => array (
      'access content'
    ),
    'type'=>MENU_CALLBACK
  );  

  // callback associée  à mettre dans tutoriel_menus.pages.inc
function tutoriel_menus_hello3_callback($mon_objet) {
  return "Hello {$mon_objet['name']}";
}
Utilisation d'un chargeur custom

Vous devez en outre ajouter une fonction de type object_load pour que Drupal puisse faire correspondre le paramètre de rang 3, à l'argument %mon_objet :
function mon_objet_load($id) {
  return array('name'=>"Objet[$id]");
}
Chargeur custom

Vous n'avez maintenant plus qu'à reconstruire les menus et tester http://mon_site/tutoriels/menus/hello3/12 pour voir apparaître "Hello Objet[12]".

Conclusion

Voilà, fin du "petit" tour d'horizon sur les menus. Comme vous l'avez vu, le sujet est pour le peu dense, à la mesure de la richesse du sujet. Ceci étant dit, nous n'avons ici qu'abordé l'essentiel, il reste encore beaucoup de choses à découvrir en explorant l'API des menus.

Retourner au sommaire
Gravatar de arNuméral
Mon premier (et dernier ;-) livre sur Drupal 
  • 0 vote
    vote oui
Par arNuméral, le 08/09/2009 à 10:39.

Petit alléluia, après des mois laborieux au possible, mon bouquin sur Drupal est enfin terminé, mis en page, parti sous presse et en est revenu tout pimpant. Avec du recul, je ne suis pas bien certain que je me serais lancé dans une telle aventure si j'avais eu pleinement conscience du temps que cela représenterait. Mais aujourd'hui, cela me fait juste bien plaisir de contempler le résultat final. Pour le reste, je ne suis sans doute pas le meilleur juge.

Ce livre n'est pas exactement un manuel mais plutôt la tentative d'un parcours (initiatique ?) à travers l'univers Drupalien. A ce titre il s'adresse à tout le monde ou presque, de la personne qui souhaite orchestrer la mise en œuvre Drupal dans un mode projet, au thèmeur qui désire comprendre comment le faire coller à ses besoins, en passant par celle ou celui qui veut simplement se laisser de la puissance en réserve pour construire son site personnel.

Pour rester le plus pragmatique possible, le fil conducteur de l'ouvrage est le cheminement d'une société qui souhaite utiliser Drupal pour motoriser sa devanture en ligne. Chaque chapitre correspond à une étape dans la mise en œuvre d'un site qui deviendra fonctionnel à la fin du livre. Cela passe par la compréhension de ce qu'est un CMS, la définition du besoin, la mise en place de la plate-forme de développement, l'installation et le paramétrage des modules correspondant à chaque fonctionnalités, la personnalisation fine de l'apparence, la mise en ligne et l'optimisation des performances. Je pense que tous les aspects d'un site sous Drupal y sont abordés. La seule exception est le développement pur et dur, avec juste un chapitre d'introduction à la création de son premier module.

Tout au long de ce parcours, j'ai essayé d'égrener tous les astuces, bonnes pratiques, modules géniaux et autres chausse-trappes que j'ai pu rencontrer en quatre ans de projets Drupaliens. En gros, c'est le livre en français que j'aurais bien aimé avoir sous la main lorsque j'ai débuté, du moins c'est ce que j'ai cherché à faire.

Voilà, vous savez l'essentiel et je vous laisse juge du reste en espérant sincèrement que cela vous plaira :

Pour ce qui est de la sortie officielle en librairie, elle se fera le 10 septembre dans toutes les bonnes crémeries. Il y en aura aussi quelques exemplaires au DrupalCon auquel je n'ai malheureusement pas pu assister. Du coup, un grand merci à Daniel pour être allé chercher les livres chez Eyrolles et les avoir trimballé jusque là.

Retourner au sommaire
Gravatar de arNuméral
Mon premier (et dernier ;-) livre sur Drupal 
  • 0 vote
    vote oui
Par arNuméral, le 08/09/2009 à 10:39.

Petit alléluia, après des mois laborieux au possible, mon bouquin sur Drupal est enfin terminé, mis en page, parti sous presse et en est revenu tout pimpant. Avec du recul, je ne suis pas bien certain que je me serais lancé dans une telle aventure si j'avais eu pleinement conscience du temps que cela représenterait. Mais aujourd'hui, cela me fait juste bien plaisir de contempler le résultat final. Pour le reste, je ne suis sans doute pas le meilleur juge.

Ce livre n'est pas exactement un manuel mais plutôt la tentative d'un parcours (initiatique ?) à travers l'univers Drupalien. A ce titre il s'adresse à tout le monde ou presque, de la personne qui souhaite orchestrer la mise en œuvre Drupal dans un mode projet, au thèmeur qui désire comprendre comment le faire coller à ses besoins, en passant par celle ou celui qui veut simplement se laisser de la puissance en réserve pour construire son site personnel.

Pour rester le plus pragmatique possible, le fil conducteur de l'ouvrage est le cheminement d'une société qui souhaite utiliser Drupal pour motoriser sa devanture en ligne. Chaque chapitre correspond à une étape dans la mise en œuvre d'un site qui deviendra fonctionnel à la fin du livre. Cela passe par la compréhension de ce qu'est un CMS, la définition du besoin, la mise en place de la plate-forme de développement, l'installation et le paramétrage des modules correspondant à chaque fonctionnalités, la personnalisation fine de l'apparence, la mise en ligne et l'optimisation des performances. Je pense que tous les aspects d'un site sous Drupal y sont abordés. La seule exception est le développement pur et dur, avec juste un chapitre d'introduction à la création de son premier module.

Tout au long de ce parcours, j'ai essayé d'égrener tous les astuces, bonnes pratiques, modules géniaux et autres chausse-trappes que j'ai pu rencontrer en quatre ans de projets Drupaliens. En gros, c'est le livre en français que j'aurais bien aimé avoir sous la main lorsque j'ai débuté, du moins c'est ce que j'ai cherché à faire.

Voilà, vous savez l'essentiel et je vous laisse juge du reste en espérant sincèrement que cela vous plaira :

Pour ce qui est de la sortie officielle en librairie, elle se fera le 10 septembre dans toutes les bonnes crémeries. Il y en aura aussi quelques exemplaires au DrupalCon auquel je n'ai malheureusement pas pu assister. Du coup, un grand merci à Daniel pour être allé chercher les livres chez Eyrolles et les avoir trimballé jusque là.

Fichier attachéTaille
Table des matières80.68 Ko
Le module d'exemple1.6 Ko
Le thème d'exemple39.5 Ko
Retourner au sommaire
Gravatar de arNuméral
Mon premier (et dernier ;-) livre sur Drupal 
  • 13 votes
    vote oui
Par arNuméral, le 08/09/2009 à 10:39.

Petit alléluia, après des mois laborieux au possible, mon bouquin sur Drupal est enfin terminé, mis en page, parti sous presse et en est revenu tout pimpant. Avec du recul, je ne suis pas bien certain que je me serais lancé dans une telle aventure si j'avais eu pleinement conscience du temps que cela représenterait. Mais aujourd'hui, cela me fait juste bien plaisir de contempler le résultat final. Pour le reste, je ne suis sans doute pas le meilleur juge.

Ce livre n'est pas exactement un manuel mais plutôt la tentative d'un parcours (initiatique ?) à travers l'univers Drupalien. A ce titre il s'adresse à tout le monde ou presque, de la personne qui souhaite orchestrer la mise en œuvre Drupal dans un mode projet, au thèmeur qui désire comprendre comment le faire coller à ses besoins, en passant par celle ou celui qui veut simplement se laisser de la puissance en réserve pour construire son site personnel.

Pour rester le plus pragmatique possible, le fil conducteur de l'ouvrage est le cheminement d'une société qui souhaite utiliser Drupal pour motoriser sa devanture en ligne. Chaque chapitre correspond à une étape dans la mise en œuvre d'un site qui deviendra fonctionnel à la fin du livre. Cela passe par la compréhension de ce qu'est un CMS, la définition du besoin, la mise en place de la plate-forme de développement, l'installation et le paramétrage des modules correspondant à chaque fonctionnalités, la personnalisation fine de l'apparence, la mise en ligne et l'optimisation des performances. Je pense que tous les aspects d'un site sous Drupal y sont abordés. La seule exception est le développement pur et dur, avec juste un chapitre d'introduction à la création de son premier module.

Tout au long de ce parcours, j'ai essayé d'égrener tous les astuces, bonnes pratiques, modules géniaux et autres chausse-trappes que j'ai pu rencontrer en quatre ans de projets Drupaliens. En gros, c'est le livre en français que j'aurais bien aimé avoir sous la main lorsque j'ai débuté, du moins c'est ce que j'ai cherché à faire.

Voilà, vous savez l'essentiel et je vous laisse juge du reste en espérant sincèrement que cela vous plaira :

Pour ce qui est de la sortie officielle en librairie, elle se fera le 10 septembre dans toutes les bonnes crémeries. Il y en aura aussi quelques exemplaires au DrupalCon auquel je n'ai malheureusement pas pu assister. Du coup, un grand merci à Daniel pour être allé chercher les livres chez Eyrolles et les avoir trimballé jusque là.

Retourner au sommaire
Gravatar de arNuméral
Drupal, Encapsuler des noeuds dans un noeud 
  • 0 vote
    vote oui
Par arNuméral, le 16/07/2009 à 09:55.

La question posée ici est : "comment peut-on mettre le contenu d'un noeud, ou d'une liste de noeuds, dans un noeud principal"... Le cas typique serait une page, avec un texte d'introduction (le noeud maître), suivi d'une liste de noeuds (dits "esclaves", résumés ou complets).

Noeuds et NID

Un noeud, quel que soit son type (page, article, etc.) dispose d'un identifiant unique appelé nid (pour Node ID). Pour information, le nid d'un noeud, est le chiffre que vous voyez dans l'URL, coincé entre node et edit, lorsque vous éditez ce noeud.

Le stockage du noeud est assuré conjointement par la table node et node_revisions. La première contient les informations fondamentales du noeud (nid, statut, langue, et surtout type), et la seconde prend en charge les données du noeud par révision (titre, corps, etc.). La valeur du champ vid (pour version ID) de la table node pointe sur la révision en cours dans la table node_revision.

Chargement d'un noeud

Mais il ne s'agit là que des informations élémentaires. En effet, un contenu créé et géré par un module contribution implique une tripotée d'autres tables. Sur l'exemple ci-contre, vous pouvez découvrir un type complexe géré par CCK qui met en oeuvre plus de table qu'on peut à priori l'imaginer.

Ainsi comme un type de contenu peut potentiellement se ramifier sur de nombreuses tables, il n'est pas possible de créer une requêtes SQL universelle qui remonterait toutes les données d'un coup. Heureusement Drupal nous fournit la fonction node_load qui effectue ce travail sans douleur à partir du nid du noeud.

Avant de passer à plus concret, voyons comment fonctionne le chargement et le rendu d'un noeud. Pour cela créons un noeud de type 'Article' (avec donc son lien pour les commentaires) qui ferra office de noeud "esclave". Dans l'exemple, le NID du noeud (ici nid:41) a été placé dans le titre pour plus de clarté.

Nous allons donc utiliser cette fameuse fonction node_load pour charger le noeud de nid 41 et toutes ses éventuelles dépendances :

$noeud_esclave=node_load(41);

Pas bien sorcier n'est-ce pas ? La fonction va ainsi charger toutes les information de la dernière révision d'un noeud, et en faire un objet PHP que nous stockons dans $noeud_esclave. Ainsi $noeud_esclave->title nous renvoie le titre du noeud, $noeud_esclave->body sont corps (tel qu'il a été saisi, et donc sans format d'entrée appliqué), etc.

Format XHTML d'un noeud

$node n'est donc pour l'instant pas formaté, il va nous falloir un peu plus pour le transformer en une version XHTML exploitable. C'est cette fois le rôle de la fonction node_view

print node_view($noeud_esclave, true, false, true);

Le premier paramètre est l'objet $noeud_esclave que nous avons obtenu de la fonction node_load. Le second paramètre true indique que nous désirons obtenir la version résumée du noeud (le "teaser"). Le troisième paramètre à false prévient la fonction que nous ne voulons pas afficher ce contenu en tant que page. Enfin, le dernier paramètre à true demande à ce que les liens additionnels soient ajoutés au contenu.

utilisation de hook_nodeapi

Nous avons maintenant la théorie, reste à voir comment concrètement intégrer cela au noeud "maître".

La meilleur des approches reste (évidemment Wink celle du module. Il vous suffit pour cela de créer un module basique et d'exploiter le hook_nodeapi. Comme son nom le suggère, ce hook permet d'intercepter chacune des étapes de la vie d'un noeud, y compris son affichage.

Partant du principe que nous voulons ajouter le résumé du noeud 41 à la fin du noeud 42, il nous faut implémenter le hook_nodeapi de la manière suivante :
function mon_module_nodeapi(&$node, $op, $teaser, $page) {
   switch ($op) {
      case 'view' : {
         if ($node->nid == 42 && !$teaser) {
            ...
         }
      }
   }
}
Implémentation de hook_nodeapi

Comme nous le disions, le hook node_api est invoqué à chaque opération (ajout, mise à jour, suppression, impression, etc.) sur un noeud. Le noeud en question est passé comme premier paramètre du hook ($node). Notez au passage qu'il s'agit là d'un passage par référence induisant que nous pouvons modifier le contenu de noeud.

Le second paramètre, $op indique l'opération effectuée. Ici nous nous intéressons uniquement à $op=='view' correspondant à l'étape finale de visualisation du noeud.

Le paramètre $teaser nous indique quant à lui si l'opération vise un affichage résumé ou complet. Nous ciblons donc notre action sur un noeud de nid 42 en affichage complet (!$teaser).

Agrégation de fragments XHTML

Pour bien comprendre ce qui suit, il faut savoir que lorsque Drupal prépare un noeud pour l'affichage, il lui ajoute un champ content qui est un tableau associatif. Ce champ va contenir des portions de code XHTML qui à eux tous vont constituer le contenu à afficher.

De manière standard, ce champ ne contient qu'une clef body (ou teaser) avec comme valeur un tableau indexé. Ce tableau a deux clefs, #value contenant le résultat formaté XHTML (en fonction du format d'entrée) de $node->body (ou node->teaser), et #weight qui contient la position de ce morceau de XHTML par rapport à d'éventuels autres.

   node : Object (
     'title' => 'NID:41 - Un noeud esclave',
     'body'  => 'consectetur adipiscing elit.
Aliquam vel tristique sapien.'
,
      ...
      'content' => array(
         'body' => array (
            '#value'=>'consectetur adipiscing elit.Aliquam vel tristique sapien.',
            '#weight'=>0))
Vue interne de l'objet $node

Pour créer le $content du modèle node.tpl.php, Drupal n'a alors plus qu'à agréger tous les éléments #value dans l'ordre des #weight.

Mise en oeuvre finale

C'est donc cette caractéristique que nous allons exploiter pour ajouter notre propre fragment de XHTML et utilisant le résultat du node_view.

function mon_module_nodeapi(&$node, $op, $teaser, $page) {
   switch ($op) {
      case 'view' : {
         if ($node->nid == 42 && !$teaser) {
            $noeud_esclave = node_load(41);
            $node->content['noeud_esclave'] = array (
    '#value' => node_view($noeud_esclave, true, false, true),
    '#weight' => 1);
         }
      }
   }
}
Implémentation finale de hook_nodeapi

Le champ #weight prend pour valeur 1, ce qui le placera au dessous du contenu du noeud maître. Une valeur négative inverserait simplement ce comportement.

Et voilà, ça fonctionne très bien et sans vilaines gluttes. Une fois que l'on a compris les deux-trois mécanismes que cette technique implique, il est possible d'aller beaucoup plus loin en ajoutant à un contenu à peu prés tout ce que l'on désire : du code XHTMl arbitraire, une liste de résumés, un formulaire, un graphique, une version complète d'un noeud, etc.

Les Worst Practices...

Views et Panels dans un bâteau...

Je n'en fais pas un mystère, je ne suis pas un Viewsonados mais il faut tout de même être honnête, la modulo-mania appliquée peut amener à des trucs assez géants avec Views...

Je suis ainsi tombé (j'ai encore un peu mal Smile, sur un exemple assez magnifique pondu par une graaaande WebAgency spécialisée Drupal, tout ça... Pour répondre à une partie de notre besoin, leur idée a été d'utiliser le module Panels et de créer un "Page Panel". De là, ils collent une vue "Views" qui ne pond qu'un seul noeud sur la partie haute, et pour celle du bas, une seconde vue "Views" qui cette fois sort une liste de noeuds... Je vous laisse imaginer la simplicité de debuggage et les performances d'un tel assemblage...

Le coup du filtre PHP Code

Il existe plusieurs autres "techniques" que j'ai croisées à droite à gauche (y compris chez moi, à mes débuts sur Drupal) qui ne valent pas, loin de là, celle du nodeapi. La plus courante et bien évidement la pire, consiste à utiliser l'horrible format d'entrée code PHP :

 insérer du code dans un noeud":
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel tristique sapien.
<?php
  $node=node_load(41);
  print node_view($node, true, false, true);
?>

Alors ça, c'est comme croiser les effluves, c'est MAL. A chaque fois que vous vient l'envie de faire un chose pareil, ayez une pensée compatissante pour le pauvre bougre qui passera dans 10 mois sur votre projet en se demandant d'où vient le comportement extraordinaire de ce contenu. En plus, mettre du code dans une base de donnée (car cela revient à cela), au delà d'être très sale, est une calamité lorsque viendra le temps de debugger.

Dans un domaine connexe mais issues ici aussi de la fameuse WebAgency dont je parlais plus haut (mais j'imagine qu'ils ne sont pas les seuls), le pompon de l'utilisation de cette "technique" a été de coller 200 lignes de code PHP dans un noeud, et d'associer le chemin de ce noeud (node/XXX) à un alias (chemin/vers/ma/fonction). Tout cela pour créer une page de fonctionnalités complexe alors qu'il aurait été si simple de mettre ce code dans une fonction, cette fonction dans un module, et d'ajouter le chemin dans un hook_menu...

Du fonctionnel dans la présentation...

L'autre (mauvaise) possibilité consiste à utiliser la fonction phptemplate_preprocess_node d'un thème (généralement dans template.php) et à ajouter le contenu XHTML du noeud esclave à la variable $vars['content'] juste avant qu'elle ne devienne le $content de node.tpl.php.

Alors ça marche mais ce n'est pas terrible. En effet, la beauté du système de thème de Drupal tient justement à ce qu'il permet une stricte séparation entre les fonctionalités d'un côté et leur présentation de l'autre. Ce que nous faisons ici relève du domaine du fonctionnel et n'a donc rien à faire dans le thème. Une bonne manière de savoir si telle ou telle chose va dans un thème ou dans un module, est de ce demander si cette chose doit survivre à un changement brutale de thème. Si c'est le cas, vous n'avez d'autre choix que le module.

De manière plus dégradée, évitez aussi de coller ce type de code, ou tout autre code d'ailleurs, directement dans node.tpl.php. En faisant cela vous commettez la même erreur que plus haut avec en prime le risque de rendre vos modèles totalement illisibles.

D'une manière générale, une petite règle qui vaut la peine d'être rappelée : Drupal est suffisamment bien conçu pour qu'un modèle ne doive contenir que les fonctions PHP if/else et print. Tout le reste doit être dans un module, ou, le cas échéant, dans template.php.

Conclusion

Nous avons vu ici qu'il est relativement simple d'intégrer proprement un noeud dans un autre, avec en prime la possibilité d'exploiter sans soucis le module Printer pour générer une version imprimable ou PDF de l'ensemble.

Retourner au sommaire
Gravatar de arNuméral
Drupal, Encapsuler des noeuds dans un noeud 
  • 0 vote
    vote oui
Par arNuméral, le 16/07/2009 à 09:55.

La question posée ici est : "comment peut-on mettre le contenu d'un noeud, ou d'une liste de noeuds, dans un noeud principal"... Le cas typique serait une page, avec un texte d'introduction (le noeud maître), suivi d'une liste de noeuds (dits "esclaves", résumés ou complets).

Noeuds et NID

Un noeud, quel que soit son type (page, article, etc.) dispose d'un identifiant unique appelé nid (pour Node ID). Pour information, le nid d'un noeud, est le chiffre que vous voyez dans l'URL, coincé entre node et edit, lorsque vous éditez ce noeud.

Le stockage du noeud est assuré conjointement par la table node et node_revisions. La première contient les informations fondamentales du noeud (nid, statut, langue, et surtout type), et la seconde prend en charge les données du noeud par révision (titre, corps, etc.). La valeur du champ vid (pour version ID) de la table node pointe sur la révision en cours dans la table node_revision.

Chargement d'un noeud

Mais il ne s'agit là que des informations élémentaires. En effet, un contenu créé et géré par un module contribution implique une tripotée d'autres tables. Sur l'exemple ci-contre, vous pouvez découvrir un type complexe géré par CCK qui met en oeuvre plus de table qu'on peut à priori l'imaginer.

Ainsi comme un type de contenu peut potentiellement se ramifier sur de nombreuses tables, il n'est pas possible de créer une requêtes SQL universelle qui remonterait toutes les données d'un coup. Heureusement Drupal nous fournit la fonction node_load qui effectue ce travail sans douleur à partir du nid du noeud.

Avant de passer à plus concret, voyons comment fonctionne le chargement et le rendu d'un noeud. Pour cela créons un noeud de type 'Article' (avec donc son lien pour les commentaires) qui ferra office de noeud "esclave". Dans l'exemple, le NID du noeud (ici nid:41) a été placé dans le titre pour plus de clarté.

Nous allons donc utiliser cette fameuse fonction node_load pour charger le noeud de nid 41 et toutes ses éventuelles dépendances :

$noeud_esclave=node_load(41);

Pas bien sorcier n'est-ce pas ? La fonction va ainsi charger toutes les information de la dernière révision d'un noeud, et en faire un objet PHP que nous stockons dans $noeud_esclave. Ainsi $noeud_esclave->title nous renvoie le titre du noeud, $noeud_esclave->body sont corps (tel qu'il a été saisi, et donc sans format d'entrée appliqué), etc.

Format XHTML d'un noeud

$node n'est donc pour l'instant pas formaté, il va nous falloir un peu plus pour le transformer en une version XHTML exploitable. C'est cette fois le rôle de la fonction node_view

print node_view($noeud_esclave, true, false, true);

Le premier paramètre est l'objet $noeud_esclave que nous avons obtenu de la fonction node_load. Le second paramètre true indique que nous désirons obtenir la version résumée du noeud (le "teaser"). Le troisième paramètre à false prévient la fonction que nous ne voulons pas afficher ce contenu en tant que page. Enfin, le dernier paramètre à true demande à ce que les liens additionnels soient ajoutés au contenu.

utilisation de hook_nodeapi

Nous avons maintenant la théorie, reste à voir comment concrètement intégrer cela au noeud "maître".

La meilleur des approches reste (évidemment Wink celle du module. Il vous suffit pour cela de créer un module basique et d'exploiter le hook_nodeapi. Comme son nom le suggère, ce hook permet d'intercepter chacune des étapes de la vie d'un noeud, y compris son affichage.

Partant du principe que nous voulons ajouter le résumé du noeud 41 à la fin du noeud 42, il nous faut implémenter le hook_nodeapi de la manière suivante :
function mon_module_nodeapi(&$node, $op, $teaser, $page) {
   switch ($op) {
      case 'view' : {
         if ($node->nid == 42 && !$teaser) {
            ...
         }
      }
   }
}
Implémentation de hook_nodeapi

Comme nous le disions, le hook node_api est invoqué à chaque opération (ajout, mise à jour, suppression, impression, etc.) sur un noeud. Le noeud en question est passé comme premier paramètre du hook ($node). Notez au passage qu'il s'agit là d'un passage par référence induisant que nous pouvons modifier le contenu de noeud.

Le second paramètre, $op indique l'opération effectuée. Ici nous nous intéressons uniquement à $op=='view' correspondant à l'étape finale de visualisation du noeud.

Le paramètre $teaser nous indique quant à lui si l'opération vise un affichage résumé ou complet. Nous ciblons donc notre action sur un noeud de nid 42 en affichage complet (!$teaser).

Agrégation de fragments XHTML

Pour bien comprendre ce qui suit, il faut savoir que lorsque Drupal prépare un noeud pour l'affichage, il lui ajoute un champ content qui est un tableau associatif. Ce champ va contenir des portions de code XHTML qui à eux tous vont constituer le contenu à afficher.

De manière standard, ce champ ne contient qu'une clef body (ou teaser) avec comme valeur un tableau indexé. Ce tableau a deux clefs, #value contenant le résultat formaté XHTML (en fonction du format d'entrée) de $node->body (ou node->teaser), et #weight qui contient la position de ce morceau de XHTML par rapport à d'éventuels autres.

   node : Object (
     'title' => 'NID:41 - Un noeud esclave',
     'body'  => 'consectetur adipiscing elit.
Aliquam vel tristique sapien.'
,
      ...
      'content' => array(
         'body' => array (
            '#value'=>'consectetur adipiscing elit.Aliquam vel tristique sapien.',
            '#weight'=>0))
Vue interne de l'objet $node

Pour créer le $content du modèle node.tpl.php, Drupal n'a alors plus qu'à agréger tous les éléments #value dans l'ordre des #weight.

Mise en oeuvre finale

C'est donc cette caractéristique que nous allons exploiter pour ajouter notre propre fragment de XHTML et utilisant le résultat du node_view.

function mon_module_nodeapi(&$node, $op, $teaser, $page) {
   switch ($op) {
      case 'view' : {
         if ($node->nid == 42 && !$teaser) {
            $noeud_esclave = node_load(41);
            $node->content['noeud_esclave'] = array (
    '#value' => node_view($noeud_esclave, true, false, true),
    '#weight' => 1);
         }
      }
   }
}
Implémentation finale de hook_nodeapi

Le champ #weight prend pour valeur 1, ce qui le placera au dessous du contenu du noeud maître. Une valeur négative inverserait simplement ce comportement.

Et voilà, ça fonctionne très bien et sans vilaines gluttes. Une fois que l'on a compris les deux-trois mécanismes que cette technique implique, il est possible d'aller beaucoup plus loin en ajoutant à un contenu à peu prés tout ce que l'on désire : du code XHTMl arbitraire, une liste de résumés, un formulaire, un graphique, une version complète d'un noeud, etc.

Les Worst Practices...

Views et Panels dans un bâteau...

Je n'en fais pas un mystère, je ne suis pas un Viewsonados mais il faut tout de même être honnête, la modulo-mania appliquée peut amener à des trucs assez géants avec Views...

Je suis ainsi tombé (j'ai encore un peu mal Smile, sur un exemple assez magnifique pondu par une graaaande WebAgency spécialisée Drupal, tout ça... Pour répondre à une partie de notre besoin, leur idée a été d'utiliser le module Panels et de créer un "Page Panel". De là, ils collent une vue "Views" qui ne pond qu'un seul noeud sur la partie haute, et pour celle du bas, une seconde vue "Views" qui cette fois sort une liste de noeuds... Je vous laisse imaginer la simplicité de debuggage et les performances d'un tel assemblage...

Le coup du filtre PHP Code

Il existe plusieurs autres "techniques" que j'ai croisées à droite à gauche (y compris chez moi, à mes débuts sur Drupal) qui ne valent pas, loin de là, celle du nodeapi. La plus courante et bien évidement la pire, consiste à utiliser l'horrible format d'entrée code PHP :

 insérer du code dans un noeud":
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel tristique sapien.
<?php
  $node=node_load(41);
  print node_view($node, true, false, true);
?>

Alors ça, c'est comme croiser les effluves, c'est MAL. A chaque fois que vous vient l'envie de faire un chose pareil, ayez une pensée compatissante pour le pauvre bougre qui passera dans 10 mois sur votre projet en se demandant d'où vient le comportement extraordinaire de ce contenu. En plus, mettre du code dans une base de donnée (car cela revient à cela), au delà d'être très sale, est une calamité lorsque viendra le temps de debugger.

Dans un domaine connexe mais issues ici aussi de la fameuse WebAgency dont je parlais plus haut (mais j'imagine qu'ils ne sont pas les seuls), le pompon de l'utilisation de cette "technique" a été de coller 200 lignes de code PHP dans un noeud, et d'associer le chemin de ce noeud (node/XXX) à un alias (chemin/vers/ma/fonction). Tout cela pour créer une page de fonctionnalités complexe alors qu'il aurait été si simple de mettre ce code dans une fonction, cette fonction dans un module, et d'ajouter le chemin dans un hook_menu...

Du fonctionnel dans la présentation...

L'autre (mauvaise) possibilité consiste à utiliser la fonction phptemplate_preprocess_node d'un thème (généralement dans template.php) et à ajouter le contenu XHTML du noeud esclave à la variable $vars['content'] juste avant qu'elle ne devienne le $content de node.tpl.php.

Alors ça marche mais ce n'est pas terrible. En effet, la beauté du système de thème de Drupal tient justement à ce qu'il permet une stricte séparation entre les fonctionalités d'un côté et leur présentation de l'autre. Ce que nous faisons ici relève du domaine du fonctionnel et n'a donc rien à faire dans le thème. Une bonne manière de savoir si telle ou telle chose va dans un thème ou dans un module, est de ce demander si cette chose doit survivre à un changement brutale de thème. Si c'est le cas, vous n'avez d'autre choix que le module.

De manière plus dégradée, évitez aussi de coller ce type de code, ou tout autre code d'ailleurs, directement dans node.tpl.php. En faisant cela vous commettez la même erreur que plus haut avec en prime le risque de rendre vos modèles totalement illisibles.

D'une manière générale, une petite règle qui vaut la peine d'être rappelée : Drupal est suffisamment bien conçu pour qu'un modèle ne doive contenir que les fonctions PHP if/else et print. Tout le reste doit être dans un module, ou, le cas échéant, dans template.php.

Conclusion

Nous avons vu ici qu'il est relativement simple d'intégrer proprement un noeud dans un autre, avec en prime la possibilité d'exploiter sans soucis le module Printer pour générer une version imprimable ou PDF de l'ensemble.

Retourner au sommaire
Gravatar de arNuméral
Drupal, Encapsuler des noeuds dans un noeud 
  • 0 vote
    vote oui
Par arNuméral, le 16/07/2009 à 09:55.

La question posée ici est : "comment peut-on mettre le contenu d'un noeud, ou d'une liste de noeuds, dans un noeud principal"... Le cas typique serait une page, avec un texte d'introduction (le noeud maître), suivi d'une liste de noeuds (dits "esclaves", résumés ou complets).

Noeuds et NID

Un noeud, quel que soit son type (page, article, etc.) dispose d'un identifiant unique appelé nid (pour Node ID). Pour information, le nid d'un noeud, est le chiffre que vous voyez dans l'URL, coincé entre node et edit, lorsque vous éditez ce noeud.

Le stockage du noeud est assuré conjointement par la table node et node_revisions. La première contient les informations fondamentales du noeud (nid, statut, langue, et surtout type), et la seconde prend en charge les données du noeud par révision (titre, corps, etc.). La valeur du champ vid (pour version ID) de la table node pointe sur la révision en cours dans la table node_revision.

Chargement d'un noeud

Mais il ne s'agit là que des informations élémentaires. En effet, un contenu créé et géré par un module contribution implique une tripotée d'autres tables. Sur l'exemple ci-contre, vous pouvez découvrir un type complexe géré par CCK qui met en oeuvre plus de table qu'on peut à priori l'imaginer.

Ainsi comme un type de contenu peut potentiellement se ramifier sur de nombreuses tables, il n'est pas possible de créer une requêtes SQL universelle qui remonterait toutes les données d'un coup. Heureusement Drupal nous fournit la fonction node_load qui effectue ce travail sans douleur à partir du nid du noeud.

Avant de passer à plus concret, voyons comment fonctionne le chargement et le rendu d'un noeud. Pour cela créons un noeud de type 'Article' (avec donc son lien pour les commentaires) qui ferra office de noeud "esclave". Dans l'exemple, le NID du noeud (ici nid:41) a été placé dans le titre pour plus de clarté.

Nous allons donc utiliser cette fameuse fonction node_load pour charger le noeud de nid 41 et toutes ses éventuelles dépendances :

$noeud_esclave=node_load(41);

Pas bien sorcier n'est-ce pas ? La fonction va ainsi charger toutes les information de la dernière révision d'un noeud, et en faire un objet PHP que nous stockons dans $noeud_esclave. Ainsi $noeud_esclave->title nous renvoie le titre du noeud, $noeud_esclave->body sont corps (tel qu'il a été saisi, et donc sans format d'entrée appliqué), etc.

Format XHTML d'un noeud

$node n'est donc pour l'instant pas formaté, il va nous falloir un peu plus pour le transformer en une version XHTML exploitable. C'est cette fois le rôle de la fonction node_view

print node_view($noeud_esclave, true, false, true);

Le premier paramètre est l'objet $noeud_esclave que nous avons obtenu de la fonction node_load. Le second paramètre true indique que nous désirons obtenir la version résumée du noeud (le "teaser"). Le troisième paramètre à false prévient la fonction que nous ne voulons pas afficher ce contenu en tant que page. Enfin, le dernier paramètre à true demande à ce que les liens additionnels soient ajoutés au contenu.

utilisation de hook_nodeapi

Nous avons maintenant la théorie, reste à voir comment concrètement intégrer cela au noeud "maître".

La meilleur des approches reste (évidemment Wink celle du module. Il vous suffit pour cela de créer un module basique et d'exploiter le hook_nodeapi. Comme son nom le suggère, ce hook permet d'intercepter chacune des étapes de la vie d'un noeud, y compris son affichage.

Partant du principe que nous voulons ajouter le résumé du noeud 41 à la fin du noeud 42, il nous faut implémenter le hook_nodeapi de la manière suivante :
function mon_module_nodeapi(&$node, $op, $teaser, $page) {
   switch ($op) {
      case 'view' : {
         if ($node->nid == 42 && !$teaser) {
            ...
         }
      }
   }
}
Implémentation de hook_nodeapi

Comme nous le disions, le hook node_api est invoqué à chaque opération (ajout, mise à jour, suppression, impression, etc.) sur un noeud. Le noeud en question est passé comme premier paramètre du hook ($node). Notez au passage qu'il s'agit là d'un passage par référence induisant que nous pouvons modifier le contenu de noeud.

Le second paramètre, $op indique l'opération effectuée. Ici nous nous intéressons uniquement à $op=='view' correspondant à l'étape finale de visualisation du noeud.

Le paramètre $teaser nous indique quant à lui si l'opération vise un affichage résumé ou complet. Nous ciblons donc notre action sur un noeud de nid 42 en affichage complet (!$teaser).

Agrégation de fragments XHTML

Pour bien comprendre ce qui suit, il faut savoir que lorsque Drupal prépare un noeud pour l'affichage, il lui ajoute un champ content qui est un tableau associatif. Ce champ va contenir des portions de code XHTML qui à eux tous vont constituer le contenu à afficher.

De manière standard, ce champ ne contient qu'une clef body (ou teaser) avec comme valeur un tableau indexé. Ce tableau a deux clefs, #value contenant le résultat formaté XHTML (en fonction du format d'entrée) de $node->body (ou node->teaser), et #weight qui contient la position de ce morceau de XHTML par rapport à d'éventuels autres.

   node : Object (
     'title' => 'NID:41 - Un noeud esclave',
     'body'  => 'consectetur adipiscing elit.
Aliquam vel tristique sapien.'
,
      ...
      'content' => array(
         'body' => array (
            '#value'=>'consectetur adipiscing elit.Aliquam vel tristique sapien.',
            '#weight'=>0))
Vue interne de l'objet $node

Pour créer le $content du modèle node.tpl.php, Drupal n'a alors plus qu'à agréger tous les éléments #value dans l'ordre des #weight.

Mise en oeuvre finale

C'est donc cette caractéristique que nous allons exploiter pour ajouter notre propre fragment de XHTML et utilisant le résultat du node_view.

function mon_module_nodeapi(&$node, $op, $teaser, $page) {
   switch ($op) {
      case 'view' : {
         if ($node->nid == 42 && !$teaser) {
            $noeud_esclave = node_load(41);
            $node->content['noeud_esclave'] = array (
    '#value' => node_view($noeud_esclave, true, false, true),
    '#weight' => 1);
         }
      }
   }
}
Implémentation finale de hook_nodeapi

Le champ #weight prend pour valeur 1, ce qui le placera au dessous du contenu du noeud maître. Une valeur négative inverserait simplement ce comportement.

Et voilà, ça fonctionne très bien et sans vilaines gluttes. Une fois que l'on a compris les deux-trois mécanismes que cette technique implique, il est possible d'aller beaucoup plus loin en ajoutant à un contenu à peu prés tout ce que l'on désire : du code XHTMl arbitraire, une liste de résumés, un formulaire, un graphique, une version complète d'un noeud, etc.

Les Worst Practices...

Views et Panels dans un bâteau...

Je n'en fais pas un mystère, je ne suis pas un Viewsonados mais il faut tout de même être honnête, la modulo-mania appliquée peut amener à des trucs assez géants avec Views...

Je suis ainsi tombé (j'ai encore un peu mal Smile, sur un exemple assez magnifique pondu par une graaaande WebAgency spécialisée Drupal, tout ça... Pour répondre à une partie de notre besoin, leur idée a été d'utiliser le module Panels et de créer un "Page Panel". De là, ils collent une vue "Views" qui ne pond qu'un seul noeud sur la partie haute, et pour celle du bas, une seconde vue "Views" qui cette fois sort une liste de noeuds... Je vous laisse imaginer la simplicité de debuggage et les performances d'un tel assemblage...

Le coup du filtre PHP Code

Il existe plusieurs autres "techniques" que j'ai croisées à droite à gauche (y compris chez moi, à mes débuts sur Drupal) qui ne valent pas, loin de là, celle du nodeapi. La plus courante et bien évidement la pire, consiste à utiliser l'horrible format d'entrée code PHP :

 insérer du code dans un noeud":
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel tristique sapien.
<?php
  $node=node_load(41);
  print node_view($node, true, false, true);
?>

Alors ça, c'est comme croiser les effluves, c'est MAL. A chaque fois que vous vient l'envie de faire un chose pareil, ayez une pensée compatissante pour le pauvre bougre qui passera dans 10 mois sur votre projet en se demandant d'où vient le comportement extraordinaire de ce contenu. En plus, mettre du code dans une base de donnée (car cela revient à cela), au delà d'être très sale, est une calamité lorsque viendra le temps de debugger.

Dans un domaine connexe mais issues ici aussi de la fameuse WebAgency dont je parlais plus haut (mais j'imagine qu'ils ne sont pas les seuls), le pompon de l'utilisation de cette "technique" a été de coller 200 lignes de code PHP dans un noeud, et d'associer le chemin de ce noeud (node/XXX) à un alias (chemin/vers/ma/fonction). Tout cela pour créer une page de fonctionnalités complexe alors qu'il aurait été si simple de mettre ce code dans une fonction, cette fonction dans un module, et d'ajouter le chemin dans un hook_menu...

Du fonctionnel dans la présentation...

L'autre (mauvaise) possibilité consiste à utiliser la fonction phptemplate_preprocess_node d'un thème (généralement dans template.php) et à ajouter le contenu XHTML du noeud esclave à la variable $vars['content'] juste avant qu'elle ne devienne le $content de node.tpl.php.

Alors ça marche mais ce n'est pas terrible. En effet, la beauté du système de thème de Drupal tient justement à ce qu'il permet une stricte séparation entre les fonctionalités d'un côté et leur présentation de l'autre. Ce que nous faisons ici relève du domaine du fonctionnel et n'a donc rien à faire dans le thème. Une bonne manière de savoir si telle ou telle chose va dans un thème ou dans un module, est de ce demander si cette chose doit survivre à un changement brutale de thème. Si c'est le cas, vous n'avez d'autre choix que le module.

De manière plus dégradée, évitez aussi de coller ce type de code, ou tout autre code d'ailleurs, directement dans node.tpl.php. En faisant cela vous commettez la même erreur que plus haut avec en prime le risque de rendre vos modèles totalement illisibles.

D'une manière générale, une petite règle qui vaut la peine d'être rappelée : Drupal est suffisamment bien conçu pour qu'un modèle ne doive contenir que les fonctions PHP if/else et print. Tout le reste doit être dans un module, ou, le cas échéant, dans template.php.

Conclusion

Nous avons vu ici qu'il est relativement simple d'intégrer proprement un noeud dans un autre, avec en prime la possibilité d'exploiter sans soucis le module Printer pour générer une version imprimable ou PDF de l'ensemble.

Retourner au sommaire
Gravatar de arNuméral
Drupal, Encapsuler des noeuds dans un noeud 
  • 0 vote
    vote oui
Par arNuméral, le 16/07/2009 à 09:55.

La question posée ici est : "comment peut-on mettre le contenu d'un noeud, ou d'une liste de noeuds, dans un noeud principal"... Le cas typique serait une page, avec un texte d'introduction (le noeud maître), suivi d'une liste de noeuds (dits "esclaves", résumés ou complets).

Noeuds et NID

Un noeud, quel que soit son type (page, article, etc.) dispose d'un identifiant unique appelé nid (pour Node ID). Pour information, le nid d'un noeud, est le chiffre que vous voyez dans l'URL, coincé entre node et edit, lorsque vous éditez ce noeud.

Le stockage du noeud est assuré conjointement par la table node et node_revisions. La première contient les informations fondamentales du noeud (nid, statut, langue, et surtout type), et la seconde prend en charge les données du noeud par révision (titre, corps, etc.). La valeur du champ vid (pour version ID) de la table node pointe sur la révision en cours dans la table node_revision.

Chargement d'un noeud

Mais il ne s'agit là que des informations élémentaires. En effet, un contenu créé et géré par un module contribution implique une tripotée d'autres tables. Sur l'exemple ci-contre, vous pouvez découvrir un type complexe géré par CCK qui met en oeuvre plus de table qu'on peut à priori l'imaginer.

Ainsi comme un type de contenu peut potentiellement se ramifier sur de nombreuses tables, il n'est pas possible de créer une requêtes SQL universelle qui remonterait toutes les données d'un coup. Heureusement Drupal nous fournit la fonction node_load qui effectue ce travail sans douleur à partir du nid du noeud.

Avant de passer à plus concret, voyons comment fonctionne le chargement et le rendu d'un noeud. Pour cela créons un noeud de type 'Article' (avec donc son lien pour les commentaires) qui ferra office de noeud "esclave". Dans l'exemple, le NID du noeud (ici nid:41) a été placé dans le titre pour plus de clarté.

Nous allons donc utiliser cette fameuse fonction node_load pour charger le noeud de nid 41 et toutes ses éventuelles dépendances :

$noeud_esclave=node_load(41);

Pas bien sorcier n'est-ce pas ? La fonction va ainsi charger toutes les information de la dernière révision d'un noeud, et en faire un objet PHP que nous stockons dans $noeud_esclave. Ainsi $noeud_esclave->title nous renvoie le titre du noeud, $noeud_esclave->body sont corps (tel qu'il a été saisi, et donc sans format d'entrée appliqué), etc.

Format XHTML d'un noeud

$node n'est donc pour l'instant pas formaté, il va nous falloir un peu plus pour le transformer en une version XHTML exploitable. C'est cette fois le rôle de la fonction node_view

print node_view($noeud_esclave, true, false, true);

Le premier paramètre est l'objet $noeud_esclave que nous avons obtenu de la fonction node_load. Le second paramètre true indique que nous désirons obtenir la version résumée du noeud (le "teaser"). Le troisième paramètre à false prévient la fonction que nous ne voulons pas afficher ce contenu en tant que page. Enfin, le dernier paramètre à true demande à ce que les liens additionnels soient ajoutés au contenu.

utilisation de hook_nodeapi

Nous avons maintenant la théorie, reste à voir comment concrètement intégrer cela au noeud "maître".

La meilleur des approches reste (évidemment Wink celle du module. Il vous suffit pour cela de créer un module basique et d'exploiter le hook_nodeapi. Comme son nom le suggère, ce hook permet d'intercepter chacune des étapes de la vie d'un noeud, y compris son affichage.

Partant du principe que nous voulons ajouter le résumé du noeud 41 à la fin du noeud 42, il nous faut implémenter le hook_nodeapi de la manière suivante :
function mon_module_nodeapi(&$node, $op, $teaser, $page) {
   switch ($op) {
      case 'view' : {
         if ($node->nid == 42 && !$teaser) {
            ...
         }
      }
   }
}
Implémentation de hook_nodeapi

Comme nous le disions, le hook node_api est invoqué à chaque opération (ajout, mise à jour, suppression, impression, etc.) sur un noeud. Le noeud en question est passé comme premier paramètre du hook ($node). Notez au passage qu'il s'agit là d'un passage par référence induisant que nous pouvons modifier le contenu de noeud.

Le second paramètre, $op indique l'opération effectuée. Ici nous nous intéressons uniquement à $op=='view' correspondant à l'étape finale de visualisation du noeud.

Le paramètre $teaser nous indique quant à lui si l'opération vise un affichage résumé ou complet. Nous ciblons donc notre action sur un noeud de nid 42 en affichage complet (!$teaser).

Agrégation de fragments XHTML

Pour bien comprendre ce qui suit, il faut savoir que lorsque Drupal prépare un noeud pour l'affichage, il lui ajoute un champ content qui est un tableau associatif. Ce champ va contenir des portions de code XHTML qui à eux tous vont constituer le contenu à afficher.

De manière standard, ce champ ne contient qu'une clef body (ou teaser) avec comme valeur un tableau indexé. Ce tableau a deux clefs, #value contenant le résultat formaté XHTML (en fonction du format d'entrée) de $node->body (ou node->teaser), et #weight qui contient la position de ce morceau de XHTML par rapport à d'éventuels autres.

   node : Object (
     'title' => 'NID:41 - Un noeud esclave',
     'body'  => 'consectetur adipiscing elit.
Aliquam vel tristique sapien.'
,
      ...
      'content' => array(
         'body' => array (
            '#value'=>'consectetur adipiscing elit.Aliquam vel tristique sapien.',
            '#weight'=>0))
Vue interne de l'objet $node

Pour créer le $content du modèle node.tpl.php, Drupal n'a alors plus qu'à agréger tous les éléments #value dans l'ordre des #weight.

Mise en oeuvre finale

C'est donc cette caractéristique que nous allons exploiter pour ajouter notre propre fragment de XHTML et utilisant le résultat du node_view.

function mon_module_nodeapi(&$node, $op, $teaser, $page) {
   switch ($op) {
      case 'view' : {
         if ($node->nid == 42 && !$teaser) {
            $noeud_esclave = node_load(41);
            $node->content['noeud_esclave'] = array (
    '#value' => node_view($noeud_esclave, true, false, true),
    '#weight' => 1);
         }
      }
   }
}
Implémentation finale de hook_nodeapi

Le champ #weight prend pour valeur 1, ce qui le placera au dessous du contenu du noeud maître. Une valeur négative inverserait simplement ce comportement.

Et voilà, ça fonctionne très bien et sans vilaines gluttes. Une fois que l'on a compris les deux-trois mécanismes que cette technique implique, il est possible d'aller beaucoup plus loin en ajoutant à un contenu à peu prés tout ce que l'on désire : du code XHTMl arbitraire, une liste de résumés, un formulaire, un graphique, une version complète d'un noeud, etc.

Les Worst Practices...

Views et Panels dans un bâteau...

Je n'en fais pas un mystère, je ne suis pas un Viewsonados mais il faut tout de même être honnête, la modulo-mania appliquée peut amener à des trucs assez géants avec Views...

Je suis ainsi tombé (j'ai encore un peu mal Smile, sur un exemple assez magnifique pondu par une graaaande WebAgency spécialisée Drupal, tout ça... Pour répondre à une partie de notre besoin, leur idée a été d'utiliser le module Panels et de créer un "Page Panel". De là, ils collent une vue "Views" qui ne pond qu'un seul noeud sur la partie haute, et pour celle du bas, une seconde vue "Views" qui cette fois sort une liste de noeuds... Je vous laisse imaginer la simplicité de debuggage et les performances d'un tel assemblage...

Le coup du filtre PHP Code

Il existe plusieurs autres "techniques" que j'ai croisées à droite à gauche (y compris chez moi, à mes débuts sur Drupal) qui ne valent pas, loin de là, celle du nodeapi. La plus courante et bien évidement la pire, consiste à utiliser l'horrible format d'entrée code PHP :

 insérer du code dans un noeud":
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel tristique sapien.
<?php
  $node=node_load(41);
  print node_view($node, true, false, true);
?>

Alors ça, c'est comme croiser les effluves, c'est MAL. A chaque fois que vous vient l'envie de faire un chose pareil, ayez une pensée compatissante pour le pauvre bougre qui passera dans 10 mois sur votre projet en se demandant d'où vient le comportement extraordinaire de ce contenu. En plus, mettre du code dans une base de donnée (car cela revient à cela), au delà d'être très sale, est une calamité lorsque viendra le temps de debugger.

Dans un domaine connexe mais issues ici aussi de la fameuse WebAgency dont je parlais plus haut (mais j'imagine qu'ils ne sont pas les seuls), le pompon de l'utilisation de cette "technique" a été de coller 200 lignes de code PHP dans un noeud, et d'associer le chemin de ce noeud (node/XXX) à un alias (chemin/vers/ma/fonction). Tout cela pour créer une page de fonctionnalités complexe alors qu'il aurait été si simple de mettre ce code dans une fonction, cette fonction dans un module, et d'ajouter le chemin dans un hook_menu...

Du fonctionnel dans la présentation...

L'autre (mauvaise) possibilité consiste à utiliser la fonction phptemplate_preprocess_node d'un thème (généralement dans template.php) et à ajouter le contenu XHTML du noeud esclave à la variable $vars['content'] juste avant qu'elle ne devienne le $content de node.tpl.php.

Alors ça marche mais ce n'est pas terrible. En effet, la beauté du système de thème de Drupal tient justement à ce qu'il permet une stricte séparation entre les fonctionalités d'un côté et leur présentation de l'autre. Ce que nous faisons ici relève du domaine du fonctionnel et n'a donc rien à faire dans le thème. Une bonne manière de savoir si telle ou telle chose va dans un thème ou dans un module, est de ce demander si cette chose doit survivre à un changement brutale de thème. Si c'est le cas, vous n'avez d'autre choix que le module.

De manière plus dégradée, évitez aussi de coller ce type de code, ou tout autre code d'ailleurs, directement dans node.tpl.php. En faisant cela vous commettez la même erreur que plus haut avec en prime le risque de rendre vos modèles totalement illisibles.

D'une manière générale, une petite règle qui vaut la peine d'être rappelée : Drupal est suffisamment bien conçu pour qu'un modèle ne doive contenir que les fonctions PHP if/else et print. Tout le reste doit être dans un module, ou, le cas échéant, dans template.php.

Conclusion

Nous avons vu ici qu'il est relativement simple d'intégrer proprement un noeud dans un autre, avec en prime la possibilité d'exploiter sans soucis le module Printer pour générer une version imprimable ou PDF de l'ensemble.

Retourner au sommaire