Créer des handlers de menu

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 ";
}
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 ";
}
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.

Vus : 278
Publié par arNuméral : 54