Créer un module sous Drupal7

Introduction

  • Drupal est un outil de gestion de contenu (CMS), comme Joomla ou Wordpress. Ce petit tutorial, destiné aux webmasters initiés, a pour but de vous faire découvrir comment créer vos propres modules, rapidement et simplement. On supposera ici que vous avez déjà installé l'outil en local ou à distance, et fait un peu le tour de l'interface d'administration.
  • Notre module d'exemple, que nous nommerons gg_contact, a pour but de créer un simple formulaire de contact avec 4 champs : société, mail, site web et commentaire. A noter que Drupal propose par défaut un formulaire de contact que l'on peut étendre au besoin via le hook (crochet) spécifique hook_alter.

1) Répertoire du module

  • Tous les modules utilisateurs de Drupal se trouvent par défaut dans le répertoire /sites/all/modules/. Pour créer un nouveau module, il suffit de créer un répertoire du nom du module lui-même, soit ici gg_contact, et de créer deux autres fichiers de base que nous aborderons dans la suite.
  • Parmi les fonctions internes de Drupal disponibles aux développeurs, celles relatives à la gestion des modules utilisent généralement une syntaxe de type NomDuModule_NomDuHookDrupal. On comprendra dès lors que le nom du module doit être définitif si on veut éviter de perdre son temps en corrections ultérieures.

2) Informations du module

  • Dans le répertoire /sites/all/modules/gg_contact/, on créé un premier fichier nommé gg_contact.info (en fait NomDuModule.info), lequel définit les informations de base du module :
  • name = GG - contact
    description = GG - Formulaire de contact personnalisé
    package = Development
    core = 7.x
    
    files[] = gg_contact.module
    
  • Dans cette petite table de hachage (couples clé/valeur), les champs name (nom) et description seront utilisés pour présenter le module dans l'interface d'administration. La clé package attachera notre nouveau module au groupe de modules Development disponible par défaut dans Drupal.
  • Une petite parenthèse s'impose à propos des modules. Dans l'interface d'administration, le menu Modules > Lister présente la liste complète des modules, classée par groupes de modules. Notre nouveau module GG - contact apparaîtra ainsi dans le groupe de modules DÉVELOPPEMENT qu'il s'agit de déplier avec le petit bouton situé devant le nom du groupe. Dans la liste dépliée, chaque ligne reprend les caractéristiques principales du module (nom et description), précédées d'une petite case à cocher pour activer/désactiver la prise en charge, en sachant que par défaut, les nouveaux modules ne sont pas activés ! Il convient donc de repérer la ligne de notre module, de cocher la case correspondante, puis de cliquer sur le bouton Enregistrer la configuration, en bas de page, pour prendre les changements en compte. On notera à ce niveau là que Drupal n'est clairement pas un modèle d'ergonomie...
  • La clé core indique la version de Drupal valide pour ce module. La version 7 que nous utilisons ici explique la valeur du champ.
  • On remarquera enfin la clé/tableau files qui permet également d'indiquer à Drupal les autres fichiers de notre module à prendre en compte, ici gg_contact.module, lequel contiendra tout le code PHP du module proprement dit.

3) Code PHP du module

  • Toujours dans le répertoire /sites/all/modules/gg_contact/, on créé un second fichier nommé gg_contact.module (en fait NomDuModule.module), que nous remplirons comme suit (les explications seront données plus loin) :
  • <?php
    
    function gg_contact_block_info() {
    	$blocks['ggcontact'] = array(
    		'info' => t('GG Contact'),
    	);
    	return $blocks;
    }
    
    function myform ($form_state) {
    
    	$form['company'] = array(
    		'#type' => 'textfield',
    		'#title' => t('Votre Société'),
    		'#default_value' => t(''),
    		'#size' => 40,
    		'#maxlength' => 64,
    		'#required' => TRUE,
    		'#nowrapper' => TRUE,
    	);
    
    	$form['email'] = array(
    		'#type' => 'textfield',
    		'#title' => t('Votre adresse mail'),
    		'#default_value' => t(''),
    		'#size' => 40,
    		'#maxlength' => 64,
    		'#required' => TRUE,
    	);
    
    	$form['url'] = array(
    		'#type' => 'textfield',
    		'#title' => t('Votre site internet'),
    		'#default_value' => t(''),
    		'#size' => 40,
    		'#maxlength' => 64,
    	);
    
    	$form['mess'] = array(
    	    '#type' => 'textarea',
    	    '#title' => t('Votre demande'),
    	    '#default_value' =>  t(''),
    	    '#cols' => 40,
    	    '#rows' => 10,
    	    '#required' => TRUE,
    	    '#resizable' => false,
    	);
    
    	$form['valider'] = array(
    		'#type' => 'submit',
    		'#value' => t('Envoyer le message'),
    	);
    
    	return $form;
    }
    
    function gg_contact_block_view ($delta='') {
    	//drupal_add_js(drupal_get_path('module', 'gg_contact').'/gg_contact.js');
    	drupal_add_css(drupal_get_path('module', 'gg_contact').'/gg_contact.css', array('group' => CSS_DEFAULT, 'every_page' => FALSE));
    	$block = array();
    	$block['content'] = drupal_get_form('myform');
    	return $block;
    }
    
    function gg_contact_theme ($existing, $type, $theme, $path) {
    	return array(
    		'myform' => array(
    			'render element' => 'form',
    		),
    	);
    }
    
    function theme_myform ($variables) {
    	$form = $variables['form'];
    	// using theme('form_element_label', array('element'=>$form['company'])) for labels add a BR...
    	$label = $field = array();
    	foreach (explode(',','company,email,url,mess') as $key) {
    		$label[$key] = preg_replace('/(.*)<\/label>([\r\n]+)$/','\1&nbsp;:</label>', theme_form_element_label(array('element'=>$form[$key])));
    		if (!strcmp($form[$key]['#type'],'textfield'))
    			$field[$key] = theme_textfield(array('element'=>$form[$key]));
    		if (!strcmp($form[$key]['#type'],'textarea'))
    			$field[$key] = theme_textarea(array('element'=>$form[$key]));
    		$rendered = drupal_render($form[$key]);
    	}
    	$output = '
    <div id="gg_contact">
    	<table>
    		<tbody>
    			<tr>
    				<th>'.$label['company'].'</th>
    				<td>'.$field['company'].'</td>
    			</tr>
    			<tr>
    				<th>'.$label['email'].'</th>
    				<td>'.$field['email'].'</td>
    			</tr>
    			<tr>
    				<th>'.$label['url'].'</th>
    				<td>'.$field['url'].'</td>
    			</tr>
    			<tr>
    				<th>'.$label['mess'].'</th>
    				<td>'.$field['mess'].'</td>
    			</tr>
    			<tr>
    				<th> </th>
    				<td>'.drupal_render($form['valider']).'</td>
    			</tr>
    		</tbody>
    	</table>
    </div>
    	'.drupal_render_children($form);
      return $output;
    }
    
    function mycheck ($form,$key,$value) {
    	$mess = t('Le champ').' "'.$form[$key]['#title'].'" ';
    	if (!strcmp($key,'company')) {
    		if (!strlen($value))
    			return $mess.t('est vide. Merci de corriger.');
    		if (strlen($value)>200)
    			return $mess.t('est limité à 200 caractères ! Merci de corriger.');
    		if (!preg_match('/^[a-z0-9éêèçàâîôûäëïöüù\-\_\@\&\!\.\,\:\µ\ \']+$/ui',$value))
    			return $mess.t('est erronné ! Merci de corriger.');
    	}
    	if (!strcmp($key,'email')) {
    		if (!strlen($value))
    			return $mess.t('est vide. Merci de corriger.');
    		if (strlen($value)>200)
    			return $mess.t('est limité à 200 caractères ! Merci de corriger.');
    		if (!filter_var($value,FILTER_VALIDATE_EMAIL))
    			return $mess.t('est erronné ! Merci de corriger.');
    	}
    	if (!strcmp($key,'url') && strlen($value)) {
    		if (strlen($value)>200)
    			return $mess.t('est limité à 200 caractères ! Merci de corriger.');
    		if (!filter_var($value,FILTER_VALIDATE_URL))
    			return $mess.t('est erronné ! Merci de corriger.');
    	}
    	if (!strcmp($key,'mess')) {
    		if (!strlen($value))
    			return $mess.t('est vide. Merci de corriger.');
    		if (strlen($value)>2000)
    			return $mess.t('est limité à 2000 caractères ! Merci de corriger.');
    		if (!preg_match('/^[a-z0-9éêèçàâîôûäëïöüù\-\_\@\&\!\.\,\:\µ\ \'\;\/\*\$\£\=\+\²\~\#\"\(\)\[\]\|\`\^\°'."\r\n\t".']+$/ui',$value))
    			return $mess.t('est erronné ! Merci de corriger.');
    	}
    	return '';
    }
    
    function myform_validate ($form, &$form_state) {
    	foreach (explode(',','company,email,url,mess') as $key) {
    		$value = $form_state['values'][$key];
    		if (strlen($err = mycheck($form,$key,$value)))
    			form_set_error($key, $err);
    	}
    }
    
    function myform_submit ($form, &$form_state) {
    	$headers = array();
    	$mess = '';
    	$headers[] = 'From:nobody@nowhere.com';
    	$headers[] = 'Content-type: text/plain; charset=utf-8';
    	foreach (explode(',','company,email,url,mess') as $key) {
    		if (!strcmp($form[$key]['#type'],'textfield'))
    			$mess .= $form[$key]['#title'].' : '.$form_state['values'][$key]."\r\n";
    		if (!strcmp($form[$key]['#type'],'textarea'))
    			$mess .= $form[$key]['#title'].' : '."\r\n----\r\n".$form_state['values'][$key]."\r\n----\r\n";
    	}
    	mail('nobody@nowhere.com','Formulaire de contact',$mess,join("\r\n",$headers));
    	drupal_set_message(t('Message envoyé !'));
    }
    
    
    ?>
    
    

Explications (et n'oubliez surtout pas le tube d'aspirine) :

  • function gg_contact_block_info() {
    	$blocks['ggcontact'] = array(
    		'info' => t('GG Contact'),
    	);
    	return $blocks;
    }
    
    • Ce hook de module (hook_block_info) indique à Drupal que notre module va créer un nouveau bloc. Un bloc est tout simplement un morceau de page web, qui peut être généré manuellement (via l'interface d'administration) ou automatiquement (via un module comme ici).
    • La fonction t() (translate ou traduire) permet de préparer l'internationalisation du nom du module. La clé du bloc (ici ggcontact) est quant-à-elle utilisée en interne. Elle doit bien entendu être unique !
    • A noter qu'à cette étape, Drupal ne sait pas encore dans quelle(s) page(s) il devra utiliser notre bloc. Nous verrons plus loin comment affecter un bloc à une page ou à une zone graphique pré-existante. Pour le moment, concentrons-nous sur le code HTML que notre module devra générer et renvoyer...
  • function myform ($form_state) {
    
    	$form['company'] = array(
    		'#type' => 'textfield',
    		'#title' => t('Votre Société'),
    		'#default_value' => t(''),
    		'#size' => 40,
    		'#maxlength' => 64,
    		'#required' => TRUE,
    		'#nowrapper' => TRUE,
    	);
    
    	$form['email'] = array(
    		'#type' => 'textfield',
    		'#title' => t('Votre adresse mail'),
    		'#default_value' => t(''),
    		'#size' => 40,
    		'#maxlength' => 64,
    		'#required' => TRUE,
    	);
    
    	$form['url'] = array(
    		'#type' => 'textfield',
    		'#title' => t('Votre site internet'),
    		'#default_value' => t(''),
    		'#size' => 40,
    		'#maxlength' => 64,
    	);
    
    	$form['mess'] = array(
    	    '#type' => 'textarea',
    	    '#title' => t('Votre demande'),
    	    '#default_value' =>  t(''),
    	    '#cols' => 40,
    	    '#rows' => 10,
    	    '#required' => TRUE,
    	    '#resizable' => false,
    	);
    
    	$form['valider'] = array(
    		'#type' => 'submit',
    		'#value' => t('Envoyer le message'),
    	);
    
    	return $form;
    }
    
    • La fonction myform se contente de décrire tous les éléments de notre formulaire de contact dans une (longue) table de hachage, laquelle sera traduite plus tard en code HTML pur.
    • Le nom de cette fonction, laissé à votre entière liberté, sera utilisé par Drupal comme nom du formulaire. Il doit être définitif car il va lui aussi servir dans des hooks de type NomDuFormulaire_NomDuHookDeFormulaireDrupal par la suite.
    • La syntaxe des champs de formulaire est assez simple et proche des éléments HTML classiques. On remarquera cependant l'option required qui indique si le champ est obligatoire, l'option resizable qui permet de rendre le champ texte multiligne retaillable à la souris, et l'option nowrapper qui indique que le champ ne doit pas être enveloppé dans une division HTML supplémentaire.
  • function gg_contact_block_view ($delta='') {
    	//drupal_add_js(drupal_get_path('module', 'gg_contact').'/gg_contact.js');
    	drupal_add_css(drupal_get_path('module', 'gg_contact').'/gg_contact.css', array('group' => CSS_DEFAULT, 'every_page' => FALSE));
    	$block = array();
    	$block['content'] = drupal_get_form('myform');
    	return $block;
    }
    
    • Ce hook de module (hook_block_view) est appelé par le moteur de rendu de Drupal quand une page (ou un autre bloc) fait appel à notre module gg_contact. C'est donc le coeur de notre application !
    • On commence ici par ajouter les scripts JS et les feuilles de style CSS de notre module, via les fonctions respectives drupal_add_js() et drupal_add_css(). On remarquera notamment l'option every_page' => FALSE qui permet de cantonner notre CSS au module courant.
    • Notre module renvoie ici une table de hachage avec une seule clé (content pour contenu), mais en fait, vous êtes libres de renvoyer autant d'éléments disctints que vous le souhaiter. Cela peut être utile notamment pour afficher une partie de la sortie en haut de la page, une autre du côté gauche, une troisième en bas, etc... Le nom des clés est libre, but always in english, please !
    • Notre $block['content'] contiendra donc l'ensemble du code HTML renvoyé par notre module. Pour cela, la structure du formulaire retournée par la fonction myform() (vue plus haut) sera injectée dans la fonction drupal_get_form(), laquelle générera le code HTML final de notre formulaire de contact.
    • Pour rentrer un peu plus dans les détails de Drupal7, la fonction drupal_get_form() fait une boucle sur chaque élément du formulaire via la fonction drupal_render($form[$key]). Cette dernière génère, via d'autres appels, le code HTML du label et du champ de formulaire respectif, séparés par un saut de ligne (ce dernier étant traduit en <br/> pur au moment du rendu), ce qui explique pourquoi les formulaires Drupal s'affichent en mode vertical par défaut.
  • function gg_contact_theme ($existing, $type, $theme, $path) {
    	return array(
    		'myform' => array(
    			'render element' => 'form',
    		),
    	);
    }
    
    • Ce hook de module (hook_theme) permet de court-circuiter le moteur de rendu par défaut.
    • Nous avons vu précédemment que Drupal affiche par défaut les labels et les champs de formulaire les uns en dessous des autres. Si ce comportement est parfois un avantage, il peut vite s'avèrer un frein à l'usage, les formulaires complexes étant rarement linéaires.
    • Pour notre formulaire de contact, nous souhaitons ainsi placer les labels et les champs de formulaire dans un tableau, la colonne de gauche contenant les noms des champs, et celle de droite les champs eux-mêmes.
    • Le nom de notre formulaire (myform) fait ici office de référence, et indique à Drupal qu'il existe quelque part dans le module une fonction theme_myform() qui sera notre fonction de rendu.
    • Le type de rendu (render element) est obligatoire ! Si nous oublions d'indiquer à Drupal que nous attendons un formulaire en retour, il n'ajoutera pas automatiquement les balises HTML classiques <form ...> ... </form...>. Le symptôme classique est un bouton de soumission du formulaire qui ne répond pas.
    • A noter enfin que si Drupal ne prend pas en compte le thème de votre module, il y a de fortes chances que vous ayez oublier d'activer le module Theme developper dans l'interface d'administration...
  • function theme_myform ($variables) {
    	$form = $variables['form'];
    	// using theme('form_element_label', array('element'=>$form['company'])) for labels add a BR...
    	$label = $field = array();
    	foreach (explode(',','company,email,url,mess') as $key) {
    		$label[$key] = preg_replace('/(.*)<\/label>([\r\n]+)$/','\1&nbsp;:</label>', theme_form_element_label(array('element'=>$form[$key])));
    		if (!strcmp($form[$key]['#type'],'textfield'))
    			$field[$key] = theme_textfield(array('element'=>$form[$key]));
    		if (!strcmp($form[$key]['#type'],'textarea'))
    			$field[$key] = theme_textarea(array('element'=>$form[$key]));
    		$rendered = drupal_render($form[$key]);
    	}
    	$output = '
    <div id="gg_contact">
    	<table>
    		<tbody>
    			<tr>
    				<th>'.$label['company'].'</th>
    				<td>'.$field['company'].'</td>
    			</tr>
    			<tr>
    				<th>'.$label['email'].'</th>
    				<td>'.$field['email'].'</td>
    			</tr>
    			<tr>
    				<th>'.$label['url'].'</th>
    				<td>'.$field['url'].'</td>
    			</tr>
    			<tr>
    				<th>'.$label['mess'].'</th>
    				<td>'.$field['mess'].'</td>
    			</tr>
    			<tr>
    				<th> </th>
    				<td>'.drupal_render($form['valider']).'</td>
    			</tr>
    		</tbody>
    	</table>
    </div>
    	'.drupal_render_children($form);
      return $output;
    }
    
    • Ce hook de formulaire (theme_NomDuFormulaire) est notre fonction de rendu, appelée par le hook de module gg_contact_theme précédent. La table de hachage $variables, fournie automatiquement par Drupal en argument, reprend toutes les informations de notre formulaire dans la clé form.
    • Pour la mise en forme de notre tableau, l'astuce est ici de faire appel à des fonctions sous-jacences de l'A.P.I. Drupal : theme_form_element_label() qui génère le code HTML du label, theme_textfield() qui génère le code HTML d'un champ texte, theme_textarea() qui génère le code HTML d'un champ texte multiligne.
    • De cette manière, nous récupérons donc chaque élement du formulaire séparément, et nous pouvons imposer la présentation comme souhaité. A noter que nous faisons quand même appel à la fonction drupal_render() pour forcer la prise en compte du champ au niveau du moteur de rendu interne, mais sans utiliser le code produit...
    • Enfin, une fois le tableau généré, il convient encore de faire appel à drupal_render_children(), laquelle permet de générer tous les éléments restants du formulaire, et notamment certains champs cachés utilisés par Drupal pour le contrôle du formulaire lui-même.
    • Nous obtenons donc au final un formulaire complet que nous pouvons facilement personnaliser à coups de CSS.
  • function mycheck ($form,$key,$value) {
    	$mess = tr('Le champ').' "'.$form[$key]['#title'].'" ';
    	if (!strcmp($key,'company')) {
    		if (!strlen($value))
    			return $mess.t('est vide. Merci de corriger.');
    		if (strlen($value)>200)
    			return $mess.t('est limité à 200 caractères ! Merci de corriger.');
    		if (!preg_match('/^[a-z0-9éêèçàâîôûäëïöüù\-\_\@\&\!\.\,\:\µ\ \']+$/ui',$value))
    			return $mess.t('est erronné ! Merci de corriger.');
    	}
    	if (!strcmp($key,'email')) {
    		if (!strlen($value))
    			return $mess.t('est vide. Merci de corriger.');
    		if (strlen($value)>200)
    			return $mess.t('est limité à 200 caractères ! Merci de corriger.');
    		if (!filter_var($value,FILTER_VALIDATE_EMAIL))
    			return $mess.t('est erronné ! Merci de corriger.');
    	}
    	if (!strcmp($key,'url') && strlen($value)) {
    		if (strlen($value)>200)
    			return $mess.t('est limité à 200 caractères ! Merci de corriger.');
    		if (!filter_var($value,FILTER_VALIDATE_URL))
    			return $mess.t('est erronné ! Merci de corriger.');
    	}
    	if (!strcmp($key,'mess')) {
    		if (!strlen($value))
    			return $mess.t('est vide. Merci de corriger.');
    		if (strlen($value)>2000)
    			return $mess.t('est limité à 2000 caractères ! Merci de corriger.');
    		if (!preg_match('/^[a-z0-9éêèçàâîôûäëïöüù\-\_\@\&\!\.\,\:\µ\ \'\;\/\*\$\£\=\+\²\~\#\"\(\)\[\]\|\`\^\°'."\r\n\t".']+$/ui',$value))
    			return $mess.t('est erronné ! Merci de corriger.');
    	}
    	return '';
    }
    
    • Cette fonction a juste pour but de valider les champs en renvoyant un message d'erreur si besoin. Elle est appelée par la fonction suivante.
  • function myform_validate ($form, &$form_state) {
    	foreach (explode(',','company,email,url,mess') as $key) {
    		$value = $form_state['values'][$key];
    		if (strlen($err = mycheck($form,$key,$value)))
    			form_set_error($key, $err);
    	}
    }
    
    • Ce hook de formulaire (hook_validate) est automatiquement appelé lors de la soumission du formulaire myform par l'internaute. Comme son nom l'indique, le but est ici de contrôler les données avant de les utiliser.
    • En cas d'erreur, la fonction retourne une description du problème via form_set_error(), qui est une fonction interne de Drupal. Si tout est ok, c'est la fonction myform_submit() qui sera automatiquement appelée par la suite avec les données recueillies.
  • function myform_submit ($form, &$form_state) {
    	$headers = array();
    	$mess = '';
    	$headers[] = 'From:nobody@nowhere.com';
    	$headers[] = 'Content-type: text/plain; charset=utf-8';
    	foreach (explode(',','company,email,url,mess') as $key) {
    		if (!strcmp($form[$key]['#type'],'textfield'))
    			$mess .= $form[$key]['#title'].' : '.$form_state['values'][$key]."\r\n";
    		if (!strcmp($form[$key]['#type'],'textarea'))
    			$mess .= $form[$key]['#title'].' : '."\r\n----\r\n".$form_state['values'][$key]."\r\n----\r\n";
    	}
    	mail('nobody@nowhere.com','Formulaire de contact',$mess,join("\r\n",$headers));
    	drupal_set_message(t('Message envoyé !'));
    }
    
    • Ce hook de formulaire (hook_submit) est la dernière fonction de notre module (ouf!).
    • Après la validation du formulaire, cette fonction traite les données reccueillies en envoyant ici un mail récapitulatif avec une petite mise en forme, puis renvoie un message de succès à l'internaute.

4) Intégration du bloc dans une page

  • L'intégration de notre bloc dans une page statique de l'interface d'administration se fait simplement via le code suivant :
  • <?php $block = module_invoke('gg_contact', 'block_view', 'myform'); print render($block['content']); ?>
    
  • La fonction module_invoke() s'occupe d'appeler notre module et récupère les blocs à afficher dans la variable $block.
  • Dans cet exemple, nous avons utilisé un seul bloc nommé $block['content'], mais rien ne vous empêche de créer des modules renvoyant plusieurs blocs... Si l'appel à la fonction module_invoke() doit être unique dans la page (toujours en premier), vous pouvez en revanche utiliser la fonction render($block[$blockname]) à plusieurs endroits différents, en appelant à chaque fois un autre bloc généré... Attention quand même aux noms des variables utilisées pour éviter les recouvrements accidentels !

5) Intégration du bloc dans une région

  • Comme tous les CMS, et bien d'autres outils web, Drupal utilise un modèle de page HTML préfabriqué (ou template), lequel contient des régions graphiques statiques, chaque région ayant son nom propre, et pouvant accueillir un ou plusieurs bloc(s) HTML.
  • Ces modèles de pages sont placés dans le répertoire /sites/all/themes/NomDuTheme/, et peuvent être étendus dans une certaines mesure. Chaque modèle peut offrir des noms de régions différents, mais en règle générale, on essaie bien entendu de suivre le modèle par défaut.
  • On pourrait quasiment parler de hook graphiques dans le sens où ces zones sont bien des crochets dans la page HTML finale. Mais il faut garder à l'esprit que ces hook graphiques n'ont rien à avoir avec les hook PHP que nous avons étudié tantôt. Dans un cas on parle de rendu, dans l'autre on parle de contrôle.
  • Dans le menu Structure > Blocs de l'interface d'administration, on retrouve notre bloc GG contact qui par défaut, n'est assigné à une aucune zone graphique spécifique. Il suffit alors de choisir une des régions du template, via la liste déroulante située juste à côté du nom du bloc, pour lier le bloc à la zone sélectionnée.

6) Bugs (sic !)

  • Malgré son âgé déjà avancé au moment de l'écriture de ce tutorial (juin 2012), Drupal7 semble souffrir de plusieurs bugs assez gênants.
  • Le premier concerne le fond des tableaux (background), et se fixe assez facilement dans la CSS globale du thème principal.
  • /* fix drupal bug with background table */
    thead th, th, td, tr:hover td,
    tr.even:hover td.active,
    tr.odd:hover td.active {
    	background: none;
    }
    
  • Le second, via le module jcarousel, est beaucoup plus casse-pied, puisqu'un simple carousel ne fonctionne pas correctement sous Chrome alors qu'il marche très bien sous Firefox...

Conclusion

  • La logique interne de Drupal se base sur les hooks (crochets), que ce soit pour la gestion des modules ou celle des formulaires. Une fois cette logique assimilée, et les principaux hook connus, la création de petits modules est finalement assez simple.
  • En revanche, dès qu'on commence à sortir des sentiers battus, les choses peuvent rapidement se corser. On l'a vu dans cet exemple pour mettre nos labels et nos champs dans un simple tableau : il a fallut court-circuiter le moteur de rendu, et surtout trouver les bonnes fonctions dans l'API pour générer le code HTML des champs... Comprendre ces moulinettes internes s'avère obligatoire si on veut maîtriser son rendu.
  • Quand à la question de savoir si l'utilisation de l'API fait gagner du temps en conception, je n'en suis pas entièrement convaincu. À part la vérification automatisée des champs vides, la validation des données reste quand même entièrement manuelle. Dériver des formulaires existant, via hook_alter, est déjà plus intéressant, mais clairement limité à des usages simples.
  • Si l'A.P.I. est bien documentée et accessible via le web, il faut quand même noter qu'elle n'est pas compatible en version ascendante. Il convient donc d'être particulièrement attentif, dans les tutoriaux trouvés sur le web, quand aux versions utilisées.
  • Enfin, comme pour toute autre brique web, une maintenance régulière de l'outil reste indispensable. Entre les bugs non critiques, les correctifs qui viennent au fil du temps, l'impossibilité de passer d'une version majeure à une autre sans extraire ses données et les réinjecter (au risque de pertes ou d'erreurs), on se rend vite compte que la maintenance correcte d'un CMS est loin d'être une sinécure, même pour un professionnel aguerrit.
  • Au final, Drupal n'est structurellement pas un mauvais outil en soi, il a même certaines fonctions très plaisantes (possibilité de créer des modèles de contenu, un module de vues assez poussé et très bien pensé sur certains points, ...), mais il est clair que son interface d'administration est un préjudice moral et artistique, pour ne pas dire une insulte au bon sens et à l'efficacité.
  • Je ne jeterais pas la pierre à Drupal : ses concurrents ne valent pas mieux à ce niveau ! :)