Drupal, Encapsuler des noeuds dans un noeud

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.

Vus : 484
Publié par arNuméral : 54