Version imprimable Version PDF RSS
jeu, 16/07/2009 - 09:55 | Yoran   Tutoriels Drupal
Drupal, Encapsuler des noeuds dans un noeud
search.png

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 ;-) 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 :-), 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.

opi , le jeu, 16/07/2009 - 15:59

Encore une fois, merci pour cet article détaillé ! J'ai surtout les sections "bonnes et mauvaises pratiques". Ca permet de progresser.

opi

Yoran, le sam, 18/07/2009 - 13:14

Ah mais c'est que c'est du vécu :) Et il ne faut hésiter à m'en filer d'autres, je suis un grand collectionneur ;-)

Artus , le jeu, 23/07/2009 - 10:01

Article sympa, comme d'hab je dirais !

Quand tu as une fonction assez générique qui est utilisée à plusieurs niveaux, tu passes par un module "générique" où tu colles un peu tout ce qui est "de haut niveau" ou tu mets ça dans template.php ?

Merci pour le point de vue :)

Tostinni , le lun, 27/07/2009 - 17:07

Je suppose que dans le serie mauvaises pratiques, tu ne dois pas etre bien fan de Computed field nan ?

Yoran, le mar, 28/07/2009 - 20:44

Ben c'est comme tout, je ne suis à priori contre rien. Mais tous ces modules peuvent être "a curse and a bless" comme dise nos amis britanniques. Et dés lors qu'il y a "code dans base de données", toute personne sensée devrait voir s'allumer un gros warning. Car ok, on va gagner du temps à la construction par rapport à un module qui ferait strictement la même chose, mais côté maintenance et debuggage c'est juste l'enfer. Du code, tout le monde peut le lire, chercher dedans, comprendre d'où vient telle ou telle fonctionnalité, etc. Là, le projet sur lequel j'interviens en ce moment, l'enfer est que pour chaque page, je dois passer un temps de malade pour comprendre quelle technique a été inventée pour la générer. Je dépense ainsi à l'aise 50% de ma charge à jouer les sherlockhomes. Alors peut-être ce client a eu son site vite et pas cher, mais aujourd'hui cela se paye... Pour te donner une idée, sur ce projet là, un outil comme sitedoc explose avant de pouvoir fournir le moindre rapport...

M61t, le sam, 01/08/2009 - 11:47

Pas mal du tout l'article.

Tobago , le jeu, 04/03/2010 - 14:28

Bravo et merci ! Certes ton article me fait rougir... Car il m'a semblé que tu parlais de moi parfois :-) Mais c'est ainsi : on apprend chaque jour.

Il m'arrive d'utiliser le modules insert_views. Je n'ai jamais pris le temps d'aller lire le code. Est-ce une "pas trop mauvaise chose" ou est-ce également à bannir ?

Personnellement, je suis un utilisateur acharné de views. En revanche, si j'ai été tenté par Panels à une époque, ce que j'ai lu à son sujet a suffit à m'en dissuader.

Merci encore.

Dab , le dim, 10/01/2010 - 18:05

En conclusion ce que tu décris dans les 'Worst Practices' sont des non confomités au modèle MVC que l'on recontre souvent dans de nombreux programmes PHP (mélange du code, des données et de la présentation). En gros ne jamais mélanger les données (M) issues de base, fichiers ... des vues (sorties html, pdf, email ...) des controleurs gérants les évenements (code métier). Le simple fait de formatter les données en dehors de la vue rend l'application imcompatible avec le modèle MVC. Attention je ne dis pas non plus que MVC soit la panacée mais cela facilite grandement les choses quant à la flexibilité et lors du debug d'une appli. J'ajouterais q'une batterie de tests permettront de rendre l'application viable dans le temps. (tests de non regression)

Yoran, le dim, 21/02/2010 - 23:35

Oui c'est un peu cela, même si je n'envisage pas réellement un MVC sans une approche objet, ce qui manque (pour le moins !) à Drupal aujourd'hui. Donc sans être réellement du MVC, il s'agit d'opérer une bonne séparation entre présentation et traitement.

Florent , le sam, 21/08/2010 - 18:33

Personnellement,

Je ne suis pas un développeur quand j'utlise du code php je range les scripts php dans un dossier et je fais des includes. Cela semble resoudre votre principale critique sur cette méthode?? ( je ne remets pas en question votre méthode cqfd, mais l'api m'est trop obscure pour le moment.

Votre code s'apparente au module Node reference?

Cordialement

Yoran, le lun, 23/08/2010 - 07:27

Zut, c'est con que le master de mon bouquin soit bouclé. Ton approche "include" pour mettre du code PHP en base est excellente et je n'y avait même pas pensé. Je le note pour l'éventuelle prochaine release, merci :)

Le code s'apparente en effet à du nodereference, mais n'implique pas la nécessité d'utiliser CCK (ce qui serait dommage si l'on a juste ce besoin là).

Florent , le lun, 23/08/2010 - 10:35

Bon Yoran,

Si j’avais su, je l'aurais dit plutôt :o)

Votre approche est super pratique, je me permets quelques petites questions. Je vois un bénéfice immédiat de votre code ( et j'entrevois le bonheur de la maîtrise de l'api). Pour ma part, je reste dans la zone modulaire avec ces avantages et inconvénients (genre finaliser un prototype à 90% et pleurer sur les 10% insolutionnables).

Bref je bouffe du test de modules à n'en plus finir (la liste est longue :o) pour valider des scénarios pérennes, ça commence a payer, j'entends par la que je commence a monter et a scénariser des prototypes sans trop bloquer. la sélection des modules fut âpre et douloureuse!!!

Je vous soumets mon cas, dites moi si c'est possible, et en bonus un petit exemple si ce n'est pas compliqué (étape 1 et 2).
_________________________________________________________________________

J'ai monté un Content Type avec un champ CCK ReferenceNode puis en progressant, je me suis dit, que je pourrais avoir d'autres besoins similaires. Solution prédéfinir plusieurs "cck node référence", puis après je me suis dit que ce serait assez pénible de remplir tous les champs et que je n'aurais jamais assez de champs en fonction des situations.

Alors, peut-on envisager cela avec votre méthode :

1 : Crée un vocabulaire, pour catégoriser, puis je lie tous les nodes associés avec cette méthode. ça suppose récupérer le TID.

2 : Peux t'on limiter aux 5 dernières nodes( date publication), ou un random sur 5 résultats au cas ou la liste est trop longue.

3 : Peux t'on envisager une pagination (pages preview- next)

4 : Cette association de nodes est-elle pérenne, cad pas lourdingue sur ce qu'il se passe derriere? ou faut il envisager votre méthode et Node reference comme intéressantes pour lier 2 à 3 nodes maximum. Dans mon cas il s’agira de n’afficher que le titre.

Merci de votre retour sur la faisabilité sur ces différents points, notamment sur le point 1 et 4. un exemple en extra, selon si….

Cordialement

ps : l'insertion de votre code n'est pas super évidente pour un newbie, par contre coller le php l'est, pourriez vous juste rappeler ou placer le code, afin de ne pas inviter les néophites a tenter le diable et a se contenter des Worst practices. :o)

Publier un nouveau commentaire

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

Plus d'informations sur les options de formatage

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