Drupal ( 39 articles - Voir la liste )

Erreur [D8] Erreur lors de l'upgrade de Drupal à propos du module Views

Lors de la mise à jour de Drupal (par exemple de 8.3.7 vers 8.4.3), vous pouvez rencontrer ce genre d'erreur :

$ drush updb -y
The following updates are pending:

views module : 
  Fix table names for revision metadata fields.

Do you wish to run all pending updates? (y/n): y
Après la mise à jour de views                                                    [ok]
Failed: InvalidArgumentException : The configuration property                      [error]
display.default.display_options.filters.my_custom_filter_id.value.2 doesn't
exist. dans Drupal\Core\Config\Schema\ArrayElement->get() (ligne 76 de
/var/www/ftvpro/web/core/lib/Drupal/Core/Config/Schema/ArrayElement.php).
Cache rebuild complete.                                                            [ok]
Finished performing updates.

Le module Views est incapable de mettre à jour la base de données correctement, à cause d'un filtre de vue.

Pour éviter ça, cherchez dans vos modules (ou ceux communautaires que vous utilisez) le filtre de vue en question. (Ici my_custom_filter.) Le module qui déclare ce filtre doit également proposer une mise à jour de la base via un fichier yaml. Pour cet exemple, le module my_module doit contenir le fichier config/schema/my_module.views.schema.yml :

# Schema for the views plugins of the my_module module.

views.filter.my_custom_filter:
  type: views.filter.in_operator
  label: 'My custom view filter'

Si ce n'est pas le cas, crééz-le et relancer la commande drush updb.

Remarque : Si on regarde le fichier comment.views.schema.yml que propose le module Comment, on y trouve aussi ce genre de déclarations :

views.argument.argument_comment_user_uid:
  type: views_argument
  label: 'Commented user ID'

views.field.comment_depth:
  type: views_field
  label: 'Comment depth'

views.row.comment_rss:
  type: views_row
  label: 'Comment'
  mapping:
    view_mode:
      type: string
      label: 'Display type'

views.sort.comment_ces_last_comment_name:
  type: views_sort
  label: 'Last comment name'

Le problème peut donc sûrement se produire pour les arguments, les champs, les lignes et les tris des vues déclarés dans les modules.

Astuce [d8] Ajouter des variables dans un render array existant

Les render array sont utilisés partout dans Drupal, pour générer des affichages. Lorsqu'on crée un bloc côté PHP, il retourne un render array. Même chose pour un formulaire ou pour un contrôleur.

Les modules peuvent définir leurs propres apparences pour les éléments : ils peuvent décrire de nouveaux render array (hook_theme()) qui seront ensuite utilisables partout.

Pour ajouter des variables à un render array proposé par un autre module, il faut tout d'abord modifier sa définition, pour que la variable puissent être transmise au template. On utilise pour cela le hook_theme_registry_alter().

/**
 * Implements hook_theme_registry_alter().
 */
function mon_module_theme_registry_alter(&$theme_registry) {
  $theme_registry['nom_du_render_array']['variables']['nouvelle_variable'] = 'valeur_par_defaut';
}

Maintenant que la variable "est autorisée", il faut l'ajouter au render array existant. Il s'agit d'un simple hook_preprocess_HOOK().

/**
 * Implements hook_preprocess().
 */
function mon_module_preprocess_nom_du_render_array(&$variables) {
  $variables['nouvelle_variable'] = 'valeur de la variable';
}

Explication :

  • nom_du_render_array est la clé définie dans le hook_theme() du module qui le propose. C'est celle qu'on renseigne lorsqu'on utilise le render array ('#theme" => 'nom_du_render_array').
  • C'est cette clé qu'on utilise dans le nom de notre fonction hook_preprocess_HOOK().

Remarque :

Comme après chaque implémentation de hook(), pensez à vider les caches.

Astuce Le module Help

Le module Help fait partie du cœur de Drupal et est activé par défaut.

Il permet d'ajouter un message au haut de la page d'édition d'un contenu : Message formulaire d'édition

Pour cela il suffit d'éditer votre type de contenu, et de remplir la zone de texte sous le champ title (le HTML est accepté) :

Configuration du module Help

Pour que le message apparaisse dans le formulaire d'édition, il faudra que le bloc Aide soit activé dans une des régions de votre thème d'administration. C'est le cas pour celui par défaut (Seven).

Astuce [D8] Ajouter un bouton d'action en back-office

Lorsque vous êtes sur la page qui liste les types de contenu par exemple, il y a le bouton Ajouter un type de contenu en haut à gauche.

Plus généralement, lorsqu'on liste des éléments (nœuds, liens de menu, ...), on propose souvent un lien pour en ajouter de nouveaux.

Si vous utilisez les vues Drupal pour ajouter des pages de liste en back-office, vous vous voudrez probablement ce genre de bouton. Cela ne se fait pas dans la configuration de la vue, mais via un fichier my_module.links.action.yml. Par exemple :

# Article
node.add.article
  route_name: node.add
  route_parameters:
    node_type: article
  title: 'Ajouter un Temps Fort'
  appears_on:
    - view.my_view_article.display_1
    - view.other_view.display_name

Explication :

On indique

  • la route (et ses paramètres) vers laquelle devra pointer le bouton d'action
  • le libellé du bouton (title)
  • sur quelle(s) page(s) il devra apparaître (= ids de routes)

Remarque :

Pour une vue, l'id de la route est composée du préfixe 'view', du nom technique de la vue, puis du nom technique de l'affichage de la vue (une vue pouvant avoir plusieurs affichages).

Ces deux ID techniques sont visibles dans l'URL lorsque vous éditez un affichage d'une vue. Ex : http://www.mysite.fr/admin/structure/views/view/`my_view_article`/edit/`display_1`.

Astuce Modifier un élément de liste dans un formulaire

Pour ajouter une classe CSS sur un élément de formulaire basique (input, select, ...), on ajoute la clé #attributes à son render array.

Par contre pas possible de le faire pour une liste de boutons radio par exemple. La classe s'ajoutera sur le conteneur à la place.

Il existe donc la clé #after_build pour remédier à ce problème (cf. documentation). Elle attend une liste de noms de fonction en valeur.

Chacune de ces fonctions sera exécutée après coup pour modifier le render array de l'élément. À ce moment de l'exécution, les sous-éléments (ici les boutons radio) ont déjà été ajoutés et peuvent donc être modifiés.

Par exemple dans ma méthode buildForm() :

$form['my_field'] = [
  '#type' => 'radios',
  '#title' => t('My field'),
  '#options' => [
    0 => t('No'),
    1 => t('Yes'),
  ],
  '#after_build' => ['_my_module_radio_add_class']
];

Et dans mon fichier my_module.module :

function _my_module_radio_add_class(array $element, FormState $form_state) {
  $options = array_keys($element['#options']);

  // Parcours des sous-éléments options
  foreach ($options as $values) {
    $element[$values]['#attributes']['class'][] = 'myclass';
  }
  return $element;
}

Astuce Rediriger en 404 ou 403

Si vous voulez rediriger l'utilisateur vers la page 404 ("Page introuvable") ou 403 ("Vous n'avez pas le droit d’accéder à cette page") de Drupal, vous utilisiez sans doute ça en Drupal 7 :

return drupal_access_denied();
return drupal_not_found();

Voici l'équivalent pour Drupal 8 :

use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

throw new AccessDeniedHttpException();
throw new NotFoundHttpException();

Astuce Drupal dans un sous-répertoire

Si vous utilisez Drupal dans un sous-répertoire servi par Apache (ex : /var/www/html/mon_site), ou derrière un reverse proxy qui ajoute un contexte à l'URL (ex : http://mon-domaine.fr/mon-contexte), il est possible que seule la page d'accueil fonctionne.

Vous aurez alors une erreur 404 sur toutes les autres pages (ex : http://mon-domaine.fr/mon-contexte/user/login).

Il vous faut alors modifier le .htaccess à la racine du site, en modifiant la ligne :

# RewriteBase /

Décommentez-la et remplacer le / par le contexte ou le sous-répertoire. Ex :

# Modify the RewriteBase if you are using Drupal in a subdirectory or in a
# VirtualDocumentRoot and the rewrite rules are not working properly.
# For example if your site is at http://example.com/drupal uncomment and
# modify the following line:
# RewriteBase /drupal
#
# If your site is running in a VirtualDocumentRoot at http://example.com/,
# uncomment the following line:
RewriteBase /mon-contexte

Erreur The following module is missing from the file system

Si vous installez un module dans Drupal et que vous en supprimez les fichiers avant de le désinstaller, vous pouvez rencontrer une erreur du genre :

The following module is missing from the file system: 
paragraphs in drupal_get_filename() (line 240 of core/includes/bootstrap.inc).

Pour Drupal, le module est désinstallé. Pourtant, il en garde des traces et cela cause cette erreur.

Deux solutions sont alors possibles :

  • Réinstaller le module, puis le désinstaller correctement
  • Supprimer la référence en base, via la requête suivante :
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='module_name';"

Astuce [D8] Installer la dernière version de drush

Sur les dépôts des distributions linux, c'est souvent une vieille version de drush qui est disponible (ex: Debian 8.4 -> drush 5.x). Voici comment installer la dernière.

Prérequis

Composer et Git doivent être installés.

Composer

sudo apt-get install curl
sudo curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

# Vérification
composer --version

GIT

sudo apt-get install git

# Vérification
git --version

Installation

  • Téléchargez drush :
sudo git clone --depth 1 https://github.com/drush-ops/drush.git /usr/local/src/drush
  • Mettez-le à jour :
cd /usr/local/src/drush
sudo composer install
  • Créez les liens symboliques suivant :
sudo ln -s /usr/local/src/drush/drush /usr/local/bin/drush
sudo ln -s /usr/local/src/drush/drush.complete.sh /etc/bash_completion.d/drush
  • Vérifiez l'installation :
drush --version

Astuce [d8] Surcharger l'affichage d'une page existante

Drupal 8 propose nativement des pages pour gérer l'inscription, la connexion, l'oubli de mot passe.

Malheureusement actuellement il n'y a pas de suggestion de template proposée. (Comme on peut le voir habituellement en commentaire dans le code source lorsque le mode debug est activé.)

Il faut donc procéder autrement et utiliser les hook_form_alter() et hook_theme() classiques.

Par exemple, pour surcharger le formulaire de la page oubli de mot de passe :

// mymodule.module

/**
 * Implements hook_form_alter()
 */
function mymodule_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {

  // Si le formulaire est celui d'oubli de mot de passe
  if ($form_id == 'user_pass') {
    $form['#theme'] = ['my_register_form'];
  }
}

/**
 * Implements hook_themer()
 */
function mymodule_theme(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {

  return [
    'my_password_form' => [
      'template' => 'user_forget_password_form',
      'render element' => 'form',
    ],
  ];
}

Explications :

  • Le hook_form_alter() permet de modifier le thème à utiliser pour le formulaire. Le thème choisi doit exister ou être déclaré dans votre module.
  • Le hook_theme() permet de déclarer le nouveau thème my_password_form et d'y affecter un template spécifique.

Remarque :

Par défaut, sans cette configuration, le template natif form.html.twig serait utilisé. Pour créer votre propre template il peut donc être pratique d'en faire une copie, la renommer (ici user_forget_password_form.html.twig) et de s'en servir comme base pour effectuer vos modifications.

Astuce [D8] Ajouter des pages de configuration

Pour rendre votre site plus facilement paramétrable, il est utile de fournir une interface d'administration pour modifier telle ou telle option.

Ces options seront ensuite accessibles partout dans votre code :

$config = \Drupal::config('mon_module.settings');
$my_option_value = $config->get('my_option');

Comme son prédécesseur, Drupal 8 permet de générer rapidement ces interfaces, ainsi que les éléments du menu d'administration correspondant :

Page d'administration en back-office

Pour générer deux pages de formulaires avec des onglets pour passer de l'un à l'autre, vous aurez besoin des fichiers suivants :

Arborescence nécesaire

Remarque :

L'exemple qui suit requiert d'activer ces deux modules : admin_toolbar et admin_toolbar_tools.

Création d'un formulaire d'administration

Voici un exemple de formulaire d'administration affichant trois champs, de types numérique, texte riche et email.

Structure générale

<?php

namespace Drupal\my_module\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Gère le formulaire de configuration générale pour le module.
 */
class GlobalSettingsForm extends ConfigFormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'my_module_settings_form';
  }

  /**
  * {@inheritdoc}
  */
  protected function getEditableConfigNames() {
    return [
      'my_module.settings',
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {

  }

}

Explications :

  • Le formulaire hérite de la classe abstraite ConfigFormBase, fournie par Drupal.
  • En plus des méthodes getFormId(), buildForm() et submitForm() habituelles, elle impose que la méthode getEditableConfigNames() soit implémentée.
  • Cette méthode permet de définir un "namespace" pour votre configuration. Chaque propriété devra avoir un id unique au sein du même "namespace".

Méthodes buildForm() et submitForm()

/**
  * {@inheritdoc}
  */
public function buildForm(array $form, FormStateInterface $form_state) {

  // Récupération de la configuration avec le "namespace" my_module.settings
  $config = $this->config('my_module.settings');

  $form['nb_news_homepage'] = array(
    '#type' => 'number',
    '#title' => $this->t('Nombre d\'actualités afichées en page d\'accueil'),
    '#default_value' => $config->get('nb_news_homepage'),
  );

  $form['welcome_text'] = array(
    '#type' => 'text_format',
    '#title' => $this->t('Texte de bienvenue'),
    '#description' => $this->t('Texte affiché en page d\'accueil.'),
    '#default_value' => $config->get('welcome_text'),
  );

  $form['contact_receiver_email'] = array(
    '#type' => 'email',
    '#title' => $this->t('Adresse email du destinataire pour le formulaire de contact'),
    '#default_value' => $config->get('contact_receiver_email'),
  );

  return parent::buildForm($form, $form_state);
}

/**
 * {@inheritdoc}
 */
public function submitForm(array &$form, FormStateInterface $form_state) {
  $this->config('my_module.settings')
    ->set('nb_news_homepage', $form_state->getValue('nb_news_homepage', 5))
    ->set('welcome_text', $form_state->getValue('welcome_text', '<p>Texte de bienvenue à changer.</p>')['value'])
    ->set('contact_receiver_email', $form_state->getValue('contact_receiver_email', 'admin@monsite.com'))
    ->save();

  parent::submitForm($form, $form_state);
}

Explications :

La méthode buildForm() est semblable à celle d'un formulaire classique. À noter cependant :

  • Pour récupérer les valeurs présentes en base et préremplir les champs, on utilise la méthode config(), avec le "namespace" définit précédemment
  • Chaque valeur est accessible individuellement, via un simple getter
  • La méthode parente est appelée

La méthode submitForm() va enregistrer les données soumises en base :

  • Les configurations actuelles sont récupérées
  • Les nouvelles valeurs sont mises à jour, puis sauvegardées
  • La méthode parente est appelée

Remarque :

Pour un champ texte riche, la méthode getValue() proposée par FormStateInterface retourne un tableau et non pas une valeur enregistrable en base.

Il faut penser à ajouter ['value'] derrière, pour avoir une chaîne de caractère utilisable.

Configuration du menu

my_module.routing.yml

C'est le fichier classique de Drupal, permettant de lier des URL à vos contrôleurs et formulaires.

# Page générale listant les pages de configuration du module
my_module.overview:
  path: '/admin/config/my_module'
  defaults:
    _controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
    _title: 'Mon module - Configuration'
    link_id: 'my_module.overview'
  requirements:
    _permission: 'administer site configuration'

# Page de configuration générale du module
my_module.settings:
  path: '/admin/config/my_module/general'
  defaults:
    _form: '\Drupal\my_module\Form\GlobalSettingsForm'
    _title: 'Mon module - Configuration générale'
  requirements:
    _permission: 'administer site configuration'

# Page de configuration des webservices du module
my_module.webservices.settings:
  path: '/admin/config/my_module/webservices'
  defaults:
    _form: '\Drupal\my_module\Form\WebservicesSettingsForm'
    _title: 'Mon module - Configuration des webservices'
  requirements:
    _permission: 'administer site configuration'

Explications :

  • Les deux dernières routes sont classiques. Elles pointent vers vos nouvelles pages de formulaire de configuration.
  • La première pointe vers un contrôleur fourni par Drupal, qui permet de lister des sous-pages (ex: http://www.monsite.com/admin/config/people)

my_module.links.menu.yml

Ce fichier définit de nouveaux éléments dans le menu d’administration.

# Page générale listant les pages de configuration du module
my_module.overview:
  title: 'Mon module'
  parent: system.admin_config
  description: 'Voir les pages de configuration du module "Mon module".'
  route_name: my_module.overview
  weight: -100

# Page de configuration générale du module
my_module.settings:
  title: 'Général'
  parent: my_module.overview
  description: 'Gérer la configuration générale du module.'
  route_name: my_module.settings
  weight: -10

# Page de configuration des webservices du module
my_module.webservice.settings:
  title: 'Webservices'
  parent: my_module.overview
  description: 'Gérer la configuration des webservices.'
  route_name: my_module.webservices.settings
  weight: -5

Explication :

Pour chaque lien souhaité, on définit :

  • le libellé (title)
  • la description au survol (attribut title pour le lien généré)
  • l'élément de menu parent
  • la route vers laquelle pointer
  • le poids de l'élément (le plus petit apparaîtra en premier)

Dans cet exemple, la page "overview" est parente des deux autres.

my_module.links.task.yml

Ce fichier définit des onglets accessibles dans les pages d'administration. Depuis la première page, on a donc un lien rapide vers la seconde, et vice-versa.

my_module.settings:
  route_name: my_module.settings
  title: 'Général'
  base_route: my_module.settings

my_module.webservices.settings:
  route_name: my_module.webservices.settings
  title: 'Webservices'
  base_route: my_module.settings

Explication :

Pour chaque onglet souhaité, on définit :

  • le libellé (title)
  • la route vers laquelle pointer
  • l'onglet principal

Astuce [D8] Commandes drush utiles

Les commandes drush pour Drupal 8 sont en partie identiques à celles pour Drupal 7. En fait, il s'agit surtout de la version de drush et non pas de celle de Drupal. Pour Drupal 8, il est conseillé d'utiliser la version 8.x de drush.

Voici une liste de commandes drush bien pratiques :

Features

Fonction Commande
Exporter un nouveau composant dans une feature drush cex
Importer les configurations du site drush cim

Modules

Fonction Commande
Information sur un module drush pmi nom_module
Télécharger un module drush dl nom_module
Activer un module drush en nom_module
Désinstaller un module drush pmu nom_module
Mettre à jour les tables en base concernant un module drush updb nom_module
Liste des modules communautaires activés drush pm-list --pipe --type=module --status=enabled --no-core
Liste des modules du cœur activés drush pm-list --pipe --type=module --status=enabled --core

Base de données

Fonction Commande
Exécuter une commande SQL drush sqlc "SELECT * FROM node;"
Créer un dump drush sql-dump > /chemin/fichier.sql
Vider une base de données drush sql-drop
Importer un dump drush sql-cli < /chemin/fichier.sql
Mettre à jour les tables en base pour tous les modules drush updb (utile après une mise à jour de sécurité)
Mettre à jour entités en base drush entup (utile après une mise à jour de module)

Autres

Fonction Commande
Vider tous les caches drush cr
Modifier le mot de passe d'un utilisateur drush upwd --password="nouveau_mot_de_passe" login_utilisateur
Exécuter une tâche planifiée drush php-eval 'monmodule_cron();'
Exécuter du code PHP drush php-eval 'echo "je suis du code php exécuté";'
Connaître la version de Drupal drush status

Astuce [D8] Ajouter un mode d'affichage à un nœud

Dans Drupal 7, pour ajouter un nouveau mode d'affichage il fallait utiliser un hook (cf: cet autre article).

Dans Drupal 8.x, tout est faisable en back-office, via Structure > Modes d'affichage. Ensuite, comme avant, il faut activer le nouveau mode pour le type de nœud correspondant :

Activation du mode d'affichage

Remarque :

Dans Drupal 8, les modes d'affichage sont cloisonnés par entité. Si vous voulez un mode d'affichage Liste pour les utilisateurs et pour les nœuds, il faudra en créer deux.

Astuce [D8] Créer un bloc

Pour créer un bloc programmatiquement, vous devez déjà avoir créé un module.

Dans cet exemple, on créera un bloc qui affiche "Hello" et le nom de l'utilisateur connecté. On pourra personnaliser qui saluer si aucun utilisateur n'est connecté.

Déclaration du bloc

Toute la déclaration/configuration du bloc se fait dans une classe PHP, placée traditionnellement dans le répertoire src/Plugin/Block/ de votre module :

<?php

namespace Drupal\my_module\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;

/**
 * @Block(
 * id = "hello_block",
 * admin_label = @Translation("My Hello block"),
 * category = @Translation("My project")
 * )
 */
class HelloBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {

    $current_user = \Drupal::currentUser();
    if (!$current_user->isAnonymous()) {
      $who = $current_user->getDisplayName();
    }
    else {
      $config = $this->getConfiguration();
      $who = isset($config['who']) ? $config['who'] : 'World';
    }

    $build = array(
      '#cache' => array(
         'contexts' => array('user'),
         'max-age' => Cache::PERMANENT,
      ),
      '#markup' => '<p>Hello ' . $who . '</p>',
    );

    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {

    $form = parent::blockForm($form, $form_state);

    $config = $this->getConfiguration();

    $form['my_block_who'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Who ?'),
      '#default_value' => isset($config['my_block_who']) ? $config['my_block_who'] : 'world',
    );

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    $this->configuration['my_module_who'] = $form_state->getValue('my_module_who');
  }

}

Explications :

  • La classe hérite de BlockBase, fournie par Drupal.
  • Des annotations permettent de préciser le nom, le nom technique et le groupe (= catégorie) du bloc.
  • La méthode principale est build(), qui retourne un tableau de theming Drupal pouvant être rendu à l'écran.
  • Les deux autres permettent d'ajouter un champ au formulaire de configuration de bloc natif fourni par Drupal.

Remarque :

Le tableau de theming définit un cache par utilisateur (#cache), et le code HTML (#markup) constituant le contenu de votre bloc.

Activation du bloc

Pour que Drupal trouve votre nouveau bloc, vous devez vider les caches.

  • Allez ensuite en back-office, dans Structure > Administration des blocs
  • Dans la région qui contiendra le bloc, cliquez sur Placer le bloc
  • Trouvez votre module dans la liste qui apparaît et cliquez sur Positionner le bloc

Positionner le bloc

Apparaît alors le formulaire d'administration du bloc.

Vous pouvez par défaut choisir pour quelles pages le module doit apparaître (en fonction de l'utilisateur, du contenu affiché en pleine page, de l'URL de la page, ...).

À ce paramétrage s'ajoute votre champ personnalisé Who ?.

Affichage

Par défaut, le bloc est affiché en suivant le template block.html.twig fourni par Drupal et le thème que vous utilisez.

Pour surcharger ce template, vous pouvez définir un nouvel habillage pour votre bloc et créer votre propre template twig.

Astuce [D8] Créer un module

La création d'un module est très rapide dans Drupal. Dans cet exemple, on créera le module mymodule.

Commencez par créer le répertoire mymodule. Vous pouvez le placer directement dans modules, ou créer un répertoire intermédiaire qui regroupera tous vos modules (ex: modules/myproject/mymodule ou modules/custom/mymodule).

Basiquement, un module n'a besoin que de deux fichiers, tous deux à la racine du répertoire : mymodule.info.yml et mymodule.module.

.info

Le fichier .info.yml permet de décrire votre module.

name: Mon module
description: Module d'exemple.
package: Mon projet
type: module
version: '8.x-1.x'
core: '8.x'
project: 'mon_module'

Explications :

  • Le package permet de regrouper les modules sur la page de liste des modules du back-office. Vous pouvez réutiliser le même package que celui d'un module existant.
  • La version est celle du module, généralement en deux partie, celle de la version du cœur et celle du module.

.module

Le fichier .module contiendra du code PHP. Pour l'instant, créez un fichier mymodule.module vide (avec uniquement <?php et au moins un retour à la ligne).

Architecture

Votre module contiendra probablement par la suite deux répertoires principaux :

  • src : répertoire contenant la grande majorité de votre code PHP, sous forme de classes d'objet
  • templates : répertoire contenant les templates fournis par votre module

Résultat

Une fois fait, vous devriez voir votre module en back-office :

Créer un module

Vous pouvez l'activer via cette interface ou utiliser drush :

drush en mymodule -y

Astuce [D8] Créer un contenu programmatiquement

Pour créer un nouveau contenu (ou n'importe quelle entité), quelques lignes suffisent :

<?php

use Drupal\node\Entity\Node;
use Drupal\Core\Entity\EntityStorageException;

$node = Node::create([
  'type' => 'article',
  'langcode' => 'fr',
  'uid' => '1',
  'status' => 1,
]);
$node->setTitle('Mon premier article');
$node->set('field_my_text', 'du texte');
$node->set('field_my_float', 150.42);
$node->set('field_my_date', date('Y-m-d'));

try {
  $node->save();
}
catch (EntityStorageException $e) {
  \Drupal::logger('mymodule')->error("La création de l'article a échouée : @message", [
    '@message' => $e->getMessage()
  ]);
  $node = NULL;
}

Explications :

  • Les arguments de la méthode create() permettent de définir le type de nœud, son créateur, son statut, sa langue, ...
  • Tous les champs sont ensuite valués via la méthode set().

Astuce [D8] Theming : définir une nouvelle apparence pour un élément

La plupart du temps dans Drupal, on définit la manière d'afficher des éléments via un tableau de theming côté PHP.

Par exemple, pour un bloc, on peut avoir un tableau du genre :

$build = array(
  '#cache' => array(
    'contexts' => array('user'),
    'max-age' => Cache::PERMANENT,
  ),
  '#markup' => '<p>Hello ' . $who . '</p>',
);

Dans cet exemple, on ne précise pas l'habillage à utiliser. Drupal sélectionnera donc un template par défaut en fonction du type de l'élément (block.html.twig dans le cas d'un bloc).

Habillage à utiliser

Pour utiliser votre propre template, il faut modifier le tableau et remplacer #markup par #theme :

$build = array(
  '#cache' => array(
    'contexts' => array('user'),
    'max-age' => Cache::PERMANENT,
  ),
  '#theme' => 'my_hello',
  '#who' => $who,
);

Déclaration de l'habillage

Pour que Drupal trouve votre habillage, vous devez implémenter le hook_theme() dans votre module.

// my_module.module

/**
 * Implements hook_theme().
 */
function my_module_theme() {
  return [
    'my_hello' => [
      'template' => 'my_hello_box',
      'variables' => [
        'who' => 'World',
      ],
    ]
  ];
}

Explications :

  • La fonction définit un nouvel habillage my_hello.
  • Le template à utiliser est my_hello_box.html.twig.
  • La variable who sera transmise au template, avec la valeur 'World' par défaut.

Template

Le template my_hello_box.html.twig placé dans le répertoire templates/ de votre module peut ressembler à ça :

{# Affichage d'un message Hello sous forme de boîte #}
<div class="box">
    <p>{{ 'Hello %who !'|t({ 'who': who }) }}</p>
</div>

Remarque :

Vous pouvez placer votre template dans n'importe quel sous répertoire de templates/. Drupal saura le trouver.

Erreur Impossible d'importer ou de télécharger les traductions de l'interface

Après l'installation d'un site chez un hébergeur, un problème peut survenir lors de l'import/du téléchargement des fichiers de traduction de l'interface :

Warning: move_uploaded_file(translations://fr.po): failed to open stream: "Drupal\locale\StreamWrapper\TranslationsStream::stream_open" call failed in Drupal\Core\File\FileSystem->moveUploadedFile() (line 79 of core/lib/Drupal/Core/File/FileSystem.php).

Drupal\Core\File\FileSystem->moveUploadedFile('/tmp/phpxTna7m', 'translations://fr.po') (Line: 856)[...]

La solution consiste à vérifier les droits sur le répertoire sites/default/files/translations. S'il n'existe pas, créez-le et donner le droit d'écriture pour l'utilisateur apache.

Erreur [D7] Les thumbnails générés ne s'affichent pas

Il arrive que Drupal génère des thumbnails avec des droits incorrects. Apache ne peut alors pas les servir et ils ne s'affichent pas dans le site.

C'est probablement parce que les droits sur le répertoire sites/default/files/ sont mauvais.

Exécutez ces commandes pour corriger ce problème :

find sites/default/files -type d -exec chmod 755 {} +
find sites/default/files -type f -exec chmod 644 {} +
chown -R www-data:www-data sites/default/files

Plus d'informations ici :

https://www.drupal.org/node/244924

Astuce [D7] Créer un webservice REST

Le fonctionnement d'un webservice REST est très proche de celui d'une page classique. On souhaite accéder à une ressource, la modifier, la supprimer, ...

On y accède via une requête HTTP, à laquelle on fournit des paramètres et/ou des données.

Voici les étapes à suivre pour créer un webservice REST dans Drupal, communicant en JSON.

URI

Pour que Drupal gère l'URL du webservice, il faut implémenter le hook_menu() habituel :

// my_module.module

/**
 * Implements hook_menu().
 */
function my_module_menu()
{
  $items = array();

  // Webservice de lecture d'un article
  $items['api/article/%'] = array(
    'title' => t('Read article'),
    'page callback' => 'my_module_ws_article_read',
    'file' => 'my_module.ws.inc,'
    'page arguments' => array(2),
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  // Webservice de mise à jour d'un article
  $items['api/article/%/update'] = array(
    'title' => t('Update article'),
    'page callback' => 'my_module_ws_article_update',
    'file' => 'my_module.ws.inc,'
    'page arguments' => array(2),
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );

  return $items;
}

Explications :

  • On souhaite que les URL de tous les webservices commencent par api/.
  • On définit 2 URL, une pour lire un article et l'autre pour le mettre à jour.
  • Les webservices seront implémentés dans les fonctions my_module_ws_article_read() et my_module_ws_article_update() du fichier my_module.ws.inc.
  • Les deux webservices devront recevoir un nid valide en paramètre dans l'URL.

Page callback

Webservice en lecture

Si le premier webservice doit retourner le nid, le titre, l'URL et le contenu de l'article, il pourrait s"implémenter ainsi :

// my_module.ws.inc

/**
 * Retourne un article au format JSON.
 *
 * @param int $nid Nid de l'article
 * @return string Le nid, le titre, l'URL et le contenu de l'article, au format JSON
 */
function my_module_ws_article_read($nid) {

  global $language;

  $data = array();

  // Requête autorisées
  drupal_add_http_header('Access-Control-Allow-Origin', "*");
  drupal_add_http_header('Access-Control-Allow-Methods', "GET, OPTIONS");
  drupal_add_http_header('Access-Control-Allow-Headers', "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Auth-Token");

  // Format du flux de sortie
  drupal_add_http_header('Content-Type', 'application/json');

  if ($_SERVER['REQUEST_METHOD'] == 'GET') {

    $article = node_load($nid);

    if (!empty($article) && $article->type == 'article') {

      $view = node_view($article);

      $title = (!empty($article->title_field[$language->language])) ?  $article->title_field[$language->language][0]['value'] : '';
      $content = (!empty($view['field_content'])) ? render($view['field_content'])) : '';

      $data = array(
        'nid' => intval($nid),
        'url_alias' => drupal_get_path_alias('node/'. $nid),
        'title' => $title,
        'content' => $content
      );
    } else {

      http_response_code('404');
      $data['error'] = 'Article introuvable (nid: ' . $nid . ').';
    }

    echo drupal_json_encode($data);
  }
}

Explications :

  • On définit le type de requête attendue par le webservice (requêtes GET ou OPTIONS depuis n'importe quel origine), ainsi que le format de sortie (ici JSON).
  • On récupère le nœud dont le nid est en paramètre. S'il est valide, on prépare le rendu de l'affichage par défaut du nœud.
  • On récupère le titre de l'article depuis le nœud, et le rendu de son champ content depuis la vue.
  • On stocke le tout dans un tableau.
  • On transforme le tableau en JSON que l'on affiche.

Remarque :

Dans cet exemple, le webservices accepte les requêtes de type OPTIONS. Cela peut être nécessaire lors de l'utilisation de certains framework (ex: Sencha). Pour ces requêtes, le contenu n'a pas besoin d'être renvoyé, d'où la condition ($_SERVER['REQUEST_METHOD'] == 'GET').

Si vous n'en avez pas besoin il est tout à fait possible de l'enlever.

Webservice en écriture

Voici le code du second webservice, pour mettre à jour le titre et le contenu de l'article. Pour simplifier, on considère que toute la mise à jour de l'article est déportée dans une fonction _my_module_article_update($article, $title, $content) :

// my_module.ws.inc

/**
 * Met à jour un article.
 *
 * @param int $nid Nid de l'article
 * @return string 'OK' si la mise à jour a réussi, un message d'erreur sinon.
 */
function my_module_ws_article_update($nid)
{
  $data = array();

  // Requête autorisées
  drupal_add_http_header('Access-Control-Allow-Origin', "*");
  drupal_add_http_header('Access-Control-Allow-Methods', "PUT, OPTIONS");
  drupal_add_http_header('Access-Control-Allow-Headers', "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Auth-Token");

  // Format du flux de sortie
  drupal_add_http_header('Content-Type', 'application/json');

  $article = node_load($nid);

  if (!empty($article) && $article->type == 'article') {

    // Récupération du corps de la requête
    $request_body_json = file_get_contents('php://input');
    $request_params = json_decode($request_body_json);
    $title = isset($request_params->title) ? $request_params->title : null;
    $content = isset($request_params->content) ? $request_params->content : null;

    if ($title != null || $content != null) {

      // Mise à jour de l'article
      $status_msg = _my_module_article_update($article, $title, $content);

      // Message de retour (ex: 'OK' si réussite, message d'erreur sinon)
      $data['content'] = $status_msg;

    } else {

      http_response_code('400');
      $data['error'] = 'Les données de mise à jour sont invalides, ou le json est mal formé (' . $request_params . ').';
    }
  } else {

    http_response_code('404');
    $data['error'] = 'Article introuvable (nid: ' . $nid . ').';
  }

  echo drupal_json_encode($data);
}

Explications :

  • On récupère le corps de la requête qui doit contenir les informations de mise à jour au format JSON. Exemple :
{
    "title": "Mon article",
    "content": "<p>Le contenu HTML de mon article</p>"
}
  • On vérifie que le JSON est valide et contient toutes les données.
  • On met à jour l'article

Remarques :

  • Il serait judicieux d'ajouter des logs dans ce webservice, en cas d'erreur d'une part, mais également en cas de réussite, puisqu'un contenu a été modifié et qu'il y a eu écriture dans la base.
  • L'accès à tout webservice en écriture devrait également être protégé. Dans l'exemple, on autorise toutes les requêtes quelle que soit leur origine. Il est important d'ajouter un filtrage sur les IP autorisées à appeler ce webservice, par exemple dans la configuration d'Apache, ou mieux, une authentification préliminaire (ex: oAuth).

Astuce [D7] Créer un script drush

Drush propose déjà pas mal de fonctionnalités. Il est en plus extensible. Voici comme ajouter votre propre script.

Déclaration du script

Créez un fichier my_module.drush.inc, et implémentez-y le hook_drush_command() :

// my_module.drush.inc

/**
 * Implements hook_drush_command().
 */
function my_module_drush_command()
{
  $items = array();
  $items['say_hello'] = array(
    'description' => t('Say "Hello"'),
    'arguments' => array(
      'who' => t('Who are you talking to ?'),
    ),
    'options' => array(
      'punctuation' => 'Which punctuation do you use ? (optional, if not provided, use ".")',
    ),
    'examples' => array(
      'drush say_hello Boby' => 'Says : "Hello Boby."',
      'drush mmsh Boby punctuation="!"' => 'Says : "Hello Boby !"',
      'drush mmsh Boby' => 'Uses the alias and says : "Hello Boby."',
    ),
    'aliases' => array('mmsh'),
    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
  );
  return $items;
}

Explications :

  • Le nom du script est say_hello. La fonction appelée derrière sera donc drush_my_module_say_hello().
  • Le script attend un prénom en argument, et éventuellement un signe de ponctuation en option.
  • Dans cet exemple, l'alias correspond aux initiales de My_Module_Say_Hello. On peut appeler le script avec ou sans cet alias :
drush say_hello
drush mmsh

Implémentation du script

Le script pourrait être implémenté ainsi :

// my_module.drush.inc

/**
 * Dit bonjour.
 * @param string $who Nom de la personne à saluer
 */
function drush_logres_business_say_hello($who) {

  $start = new DateTime();

  if (empty($who)) {

    echo 'Say "Hello" to who ?';

  } else {

    // Récupération des options
    $punctuation = drush_get_option('punctuation');
    $punctuation = (!empty($punctuation)) ? ' ' . $punctuation : '.';

    // Traitement
    echo 'Hello ' . $who . $punctuation;
  }

  // Affichage du temps d'exécution
  $end = new DateTime();
  echo "\n" . t('Duration : !duration', array('!duration' => $end->diff($start)->format('%hH %imin %ss')) ) . "\n";
}

Explications :

  • On vérifie la valeur en argument
  • On récupère une éventuelle option
  • On effectue le traitement souhaité (ici, on dit bonjour)
  • On affiche le temps qu'a duré le script

Erreur [D7] Le nouvel alias d'URL n'est pas pris en compte

Lorsque vous utilisez les alias d'URL de Drupal et pathauto pour les générer automatiquement, il vous arrive peut-être ce problème.

  • Vous créez un contenu et vous laissez pathauto générer son alias URL par défaut.
  • Ensuite, vous modifiez le contenu et saisissez un alias personnalisé.
  • Et pourtant, le contenu a toujours l'URL automatique.

Le problème se produit probablement lorsque vous avez un site multilingue, que vous modifiez le pattern de génération de l'URL alors que vous avez déjà des contenus existants, et que vous utiliser l'option Créer un nouvel alias et conserver l'alias existant.

Une solution semble de modifier la fonction path_load() du fichier path.inc du cœur de Drupal.

Ajoutez cette ligne à la requête récupérant l'alias d'URL :

->orderBy('pid', 'DESC')

De cette manière, vous êtes sûr que Drupal choisira le dernier alias généré (= alias personnalisé) et non pas l'ancien (automatique).

Remarque :

Je n'ai pas réussi à isoler le problème sur une installation vierge, avec un minimum de modules. Je l'ai reproduit sur deux sites très similaires assez importants. Dans les deux cas, l'ajout de cette ligne à solutionner le problème.

Astuce [D7] Ajouter des pages de configuration

Pour gérer certains paramètres propres à votre site, vous utilisez souvent les fonctions variable_get() et variable_set() de Drupal.

Drupal fournit une API pour pouvoir très rapidement créer un formulaire d'édition pour ces variables :

Menu - page de configuration en BO Page de configuration en BO

Voici les différentes étapes pour ajouter une page de configuration.

hook_menu()

Pour que Drupal référence vos nouvelles pages, déclarez-les dans un hook_menu() :

// mon_module.module

/**
 * Implements hook_menu().
 */
function my_module_menu()
{
  // Page racine "Mon site"
  $items['admin/config/mon_site'] = array(
    'title' => t('My site'),
    'description' => t('Administration of the site.'),
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array('administer mysite'),
    'position' => 'right',
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
  );

  // Page "Général"
  $items['admin/config/mon_site/general'] = array(
    'title' => t('General'),
    'description' => t('Adjust global settings.'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('my_module_admin_form_site_general'),
    'access arguments' => array('administer mysite'),
    'file' => 'my_module.pages.inc',
    'type' => MENU_LOCAL_TASK,
  );

  // Page "Webservices"
  $items['admin/config/mon_site/webservices'] = array(
    'title' => t('General'),
    'description' => t('Adjust global settings.'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('my_module_admin_form_site_ws'),
    'access arguments' => array('administer mysite'),
    'file' => 'my_module.pages.inc',
    'type' => MENU_LOCAL_TASK,
  );

  return $items;
}

Explications :

  • La première déclaration permet définir le premier élément de menu (Mon site).
  • Les deux suivantes définissent les sous-éléments de ce menu (Général et Webservices).
  • Les deux formulaires de configuration seront définis respectivement dans les fonctions de callback my_module_admin_form_site_general() et my_module_admin_form_site_ws().
  • Ces fonctions seront recherchées dans le fichier my_module.pages.inc.
  • Pour accéder à ces page, il faudra avoir la permission administer mysite.

Form callback

// my_module.page.inc

/**
 * Form callback: administration variables
 */
function my_module_admin_form_site_general() {

  // Création d'un fieldset
  $form['search'] = array(
    '#type' => 'fieldset',
    '#title' => t('Search'),
  );

  // Ajout d'un champ texte dans ce fieldset
  $form['search']['my_module_nb_results_per_page'] = array(
    '#type' => 'textfield',
    '#title' => t('Number of results per page'),
    '#default_value' => variable_get('my_module_nb_results_per_page'),
    '#description' => t('Some description'),
  );

  // Ajout d'une liste déroulante dans ce fieldset
  $options = array(
    'value1' => t('Label 1'),
    'value2' => t('Label 2'),
  );
  $form['search']['my_module_first_result'] = array(
    '#type' => 'select',
    '#title' => t('First result'),
    '#options' => $options,
    '#default_value' => variable_get('my_module_first_result', 0),
    '#description' => t('Some description'),
  );

  return system_settings_form($form);
}

Explications :

  • Le fieldset correspond à celui HTML, il permet de regrouper des champs.
  • L'imbrication d'un champ dans un fieldset est reproduite dans le tableau php $form.
  • L'enregistrement du formulaire est géré automatiquement par Drupal.
  • Vous gérez seulement la valeur par défaut des champs (= valeur présente dans la variable correspondante, récupérée via variable_get()).

Remarque :

Le formulaire décrit dans cette fonction fonctionne exactement comme tout autre formulaire Drupal. Vous pouvez y ajouter des règles de validation de champs, ...

Astuce [D7] Ajouter du JS à la fin du body

Par défaut, Drupal rend la variable $scripts disponible dans votre html.tpl.php. Elle contient toutes les lib javascript ainsi que tous le code javascript inline que vous avez ajouté à Drupal via :

  • le fichier .info de votre thème (mon_theme.info)
  • la fonction drupal_add_js()

Vous avez donc quelque chose comme ça au début de votre fichier html.tpl.php :

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title><?php print $head_title; ?></title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <?php print $styles; ?>
  <?php print $scripts; ?>
  <?php print $head; ?>
</head>

Si vous déplacez <?php print $scripts; ?> tout à la fin du body, vous aurez sûrement des problèmes, car certains modules vont vouloir utiliser des lib javascript en amont (par exemple jQuery).

Un solution consiste à laissez $scripts où il se trouve et à créer une nouvelle variable $footer_scripts. Elle sera ajoutée à la fin du body et contiendra tout le javascript "non natif" dont aurez besoin pour votre site :

  <?php if (isset($footer_scripts)) { print $footer_scripts; } ?>
</body>
</html>

Voici les différentes étapes à suivre pour pouvoir utiliser cette variable.

Ajout de la variable au template html.tpl.php

Pour cela, utilisez le hook_process_html() :

// mon_module.module

/**
 * Implements hook_process_html().
 */
function mon_module_process_html(&$variables) {

  // Ajout des scripts JS à mettre en pied de page dans la variable $footer_scripts
  $variables['footer_scripts'] = drupal_get_js('footer');
}

Explication :

La variable $footer_scripts aura pour valeur le code HTML permettant d'inclure tout le code JS dont le scope est footer.

Ajout de javascript avec le scope footer

Pour cela, utilisez le hook_preprocess_page() :

// mon_theme/template.php

/**
 * Implements hook_preprocess_page().
 */
function mon_theme_preprocess_page(&$vars) {

  // Ajout de code javascript inline
  $js = 'alert("Ceci est du code Javascript.");';
  drupal_add_js($js, array('type' => 'inline', 'scope' => 'footer'));

  // Exemple d'ajout de code javascript externe
  drupal_add_js(
    'http://maps.googleapis.com/maps/api/js?v=3', 
    array('type' => 'external', 'scope' => 'footer')
  );
}

Astuce [D7] Afficher le numéro de version du site en back-office

Il est utile de numéroter les différentes versions d'un site web, particulièrement quand il doit être déployé sur plusieurs environnements (ex: dev, recette, production, ...). Ce numéro peut correspondre à un tag svn par exemple.

Pour savoir dans quelle version se trouve chaque instance, voici comment l'afficher dans la page d'information système de Drupal en back-office :

Version du site en BO

hook_field_widget_form_alter()

Pour modifier le formulaire présent sur la page d'information système, il faut utiliser le hook_field_widget_form_alter().

Dans ce hook, vous pouvez modifier un formulaire pour y ajouter/enlever des champs. Dans notre cas, l'objectif est d'ajouter un champ version, non modifiable :

/**
 * Implements hook_field_widget_form_alter().
 */
function mon_module_form_alter(&$form, &$form_state, $form_id) {

  switch($form_id) {

    case 'system_site_information_settings':
      $form['site_information']['mon_site_version'] = array(
        '#type' => 'textfield',
        '#title' => t('Version'),
        '#value' => variable_get('mon_site_version'),
        '#attributes' => array('readonly' => 'readonly')
      );
      break;

    default:
      break;
  }
}

Explications :

  • On ajoute un champ de type textfield, avec pour libellé Version, en lecture seule (= avec l'attribut HTML readonly).
  • La valeur du champ sera une variable drupal ayant pour nom mon_site_version.

Remarque :

Pour mettre à jour le numéro de version automatiquement à la mise à jour du module, voir l'exemple dans cet article : hook_update() dans D7.

Astuce [D7] hook_update()

Description

Dans Drupal, le hook_update() permet d'exécuter du code PHP à la mise à jour d'un module.

Imaginons par exemple que vous ayez un numéro de version de votre site web, enregistré dans une variable drupal. Lors de la mise à jour de votre site, vous souhaiter incrémenter ce numéro de version.

Il suffit d'utiliser le hook_update(), et lorsque le module sera mis à jour, l'incrémentation sera appliquée automatiquement.

De plus, si vous avez plusieurs hook_update(), il seront tous exécutés un à un, dans l'ordre, lors de la mise à jour. Lors des mises à jour suivantes seuls les nouveaux hooks n'ayant pas encore été exécutés le seront.

Application

Hook

Par convention, ce hook doit être utilisé dans le fichier mon_module.install :

/**
 * Update v1.0.1
 */
function mon_module_update_7101() {
  variable_set('mon_site_version', '1.0.1');
}

Explication :

Le numéro à la fin du hook correspond à la version du module. Le premier chiffre, par convention, désigne la version majeure de drupal utilisée.

Mise à jour du module

Une fois la nouvelle version du module déployée sur votre site, vous devez lancer la mise à jour. Cela peut-être fait en back-office dans la page de gestion des modules, ou mieux, via une commande drush :

drush updb -y

Mises à jours successives

Imaginons que vous ayez passé cette première mise à jour (7101) sur l'un de vos sites (exemple : l'instance de production), et qu'entre temps vous ayez créé deux nouvelles versions du module.

Le fichier mon_module.install devient :

/**
 * Update v1.0.1
 */
function mon_module_update_7101() {
  variable_set('mon_site_version', '1.0.1');
}

/**
 * Update v1.0.2
 */
function mon_module_update_7102() {
  variable_set('mon_site_version', '1.0.2');

  // Autres traitements
}

/**
 * Update v1.0.3
 */
function mon_module_update_7103() {
  variable_set('mon_site_version', '1.0.3');

  // Autres traitements
}

Si vous lancez la mise à jour sur votre instance de production, les fonctions mon_module_update_7102() et mon_module_update_7103() seront exécutées dans cet ordre.

La fonction mon_module_update_7101() elle, ne sera pas exécutée.

Astuce [D7] Effectuer des requêtes sur une autre base de données

Il est possible d'effectuer des requêtes SQL sur une base de données autre que celle de Drupal, tout en utilisant les fonctions db_select(), db_query(), ....

Pour cela, il faut déclarer la ou les bases externes dans le fichier site/default/settings.php :

$databases = array (
  'default' => array (
    'default' => array (
      'database' => 'drupal',
      'username' => 'username',
      'password' => 'password',
      'host' => 'localhost',
      'port' => '',
      'driver' => 'mysql',
      'prefix' => '',
    ),
  ),
  'ma_nouvelle_base' => array (
    'default' => array (
      'database' => 'db1',
      'username' => 'username2',
      'password' => 'password2',
      'host' => 'db.example.com',
      'port' => '',
      'driver' => 'mysql',
      'prefix' => '',
    ),
  ),
);

Vous pouvez maintenant utiliser la nouvelle base dans vos modules, grâce à la fonction db_set_active() :

// Sélection de la nouvelle base
db_set_active('ma_nouvelle_base');

// Exécution d'un requête
$results = db_query($sql);

// Retour à la base par défaut
db_set_active('default');

Astuce [D7] Activer automatiquement tous les modules d'un profil

Si vous utiliser Drush dans Drupal, vous pouvez activer automatiquement tous les modules déclarés en dépendance de votre profil.

Pour cela utilisez cette commande :

drush en $(grep dependencies /path/to/my-site/profiles/my_profile/my_profile.info | sed -n 's/dependencies\[\]=\(.*\)/\1/p')

Astuce [D7] Ajouter un mode d'affichage à un noeud

Pour créer un mode d'affichage programmatiquement, vous devez déjà avoir créé un module.

Par défaut, Drupal propose les modes d'affichage suivant : Contenu complet (= Full), Accroche (= Teaser) et RSS. Le hook_entity_info_alter() dans le fichier mymodule.module permet d'en ajouter de nouveaux.

/**
 * Implements hook_entity_info_alter();
 */
function mymodule_entity_info_alter(&$entity_info) {

  $entity_info['node']['view modes']['my_view_mode'] = array(
    'label' => t('My view mode'),
    'custom settings' => FALSE,
  );
}

Après avoir vidé les caches, vous devriez voir votre nouveau mode d'affichage en back-office :

Activation du mode d'affichage

Astuce [D7] Ajouter des variables au template node.tpl.php

Lorsque vous affichez un nœud dans le template node.tpl.php ou une de ses surcharges (ex: node--article.tpl.php), vous avez souvent besoin d'effectuer des traitements particuliers.

Pour séparer la partie traitement de l'affichage, il est préférable de mettre le maximum de code PHP dans votre fichier mymodule.module (ou mieux, dans d'autres fichiers PHP à vous). Pour cela, utilisez le hook_node_view().

Par exemple si dans le template d'un article on veut afficher les 3 derniers articles publiés :

/**
 * Implements hook_node_view().
 */
function mymodule_node_view($node, $view_mode, $langcode) {

  global $language;
  if ($node->type === 'article) {

    // Last published articles
    $query = db_select('node', 'n')
      ->fields('n', array('nid'))
      ->condition('status', 1)
      ->condition('bundle', 'article')
      ->orderBy('changed', 'DESC')
      ->range(0, 3);
    $nids = $query->execute()->fetchCol();
    $nodes = !empty($nids) ? node_load_multiple($nids) : array();

    $node->content['last_published_articles'] = $nodes;
  }
}

Explications :

  • On vérfie le type de nœud, pour n'effectuer le traitement que pour les articles
  • La requête récupère les nids des nœuds recherchés. Ils sont ensuite chargés.
  • Les nœuds sont envoyés en paramètre au template. La variable $content['last_published_articles'] sera disponible dans le template.

Remarque :

  • En général on ajoute une condition sur le type d’affichage (Contenu complet, Accroche, ...) disponible via la variable $view_mode, pour éviter d'effectuer le traitement là où c'est inutile.

Astuce [D7] Créer un bloc

Pour créer un bloc programmatiquement, vous devez déjà avoir créé un module.

Deux hooks vont être nécessaires dans le fichier mymodule.module. L'un pour déclarer votre bloc, l'autre pour définir son contenu.

hook_block_info()

/**
 * Implements hook_block_info();
 */
function mymodule_block_info() {

  $blocks['myblock'] = array(
    'info' => t('My block'),
    'cache' => DRUPAL_CACHE_PER_ROLE,
  );

  $blocks['myotherblock'] = array(
    'info' => t('My other block'),
    'cache' => DRUPAL_CACHE_PER_PAGE,
  );

  return $blocks;
}

Explications :

  • Ce hook retourne la liste des blocs à définir, avec pour chacun d'eux, son nom, le type de cache à utiliser, ... (Cf. Documentation)
  • Vous pouvez ajouter autant de blocs que vous le souhaitez dans le tableau de résultats.

hook_block_view()

/**
 * Implements hook_block_view();
 */
function mymodule_block_view($delta = '') {

  switch ($delta) {

    case 'myblock' :
      $block['subject'] = t('My block');
      $block['content'] = '<p>Contenu de mon bloc.</p>';
      break;

    case 'myotherblock' :
      $block['content'] = 'Contenu de mon second bloc.';
      break;
  }

  return $block;
}

Explications :

  • Ce hook reçoit le nom d'un bloc en argument et retourne son contenu sous forme de tableau de rendu. (Cf. Documentation)
  • Le tableau retourné doit au moins contenir la clé content, avec du texte simple ou du html en valeur.
  • Souvent, chaque case effectue une ou plusieurs requêtes en base pour récupérer des données, puis prépare le texte à afficher.

Remarque :

Si vous affichez des nœuds dans votre bloc, vous pourrez appeler directement la fonction node_view() pour générer leur contenu html. Ex :

$content = '<h2>' . t('Last published') . '</h2>';
$content .= '<ul>';
foreach ($node_list as $node) {

  $content .= '<li>';
  $content .= node_view($node, 'list');
  $content .= '</li>';
}
$content .= '</ul>'; 

$block['content'] = $content;

Astuce [D7] Créer un module

La création d'un module est très rapide dans Drupal. Dans cet exemple, on créera le module mymodule.

Commencez par créer le répertoire mymodule/. Vous pouvez le placer directement dans site/all/modules, ou créer un répertoire intermédiaire qui regroupera tous vos modules (ex: site/all/modules/myproject/mymodule/, ou site/all/modules/custom/mymodule/).

Basiquement, un module n'a besoin que de deux fichiers, tous deux à la racine du répertoire : mymodule.info et mymodule.module.

.info

Le fichier .info permet de décrire votre module.

name = mymodule
description = module de test
package = Mypackage
core = 7.x
version = "7.x-1.0"

Explications :

  • Le package permet de regrouper les modules sur la page de liste des modules du back-office. Vous pouvez réutiliser le même package que celui d'un module existant.
  • La version est celle du module, généralement en deux parties, celle de la version du cœur de Drupal et celle du module
  • Cf. documentation officielle

.module

Le fichier .module contiendra une bonne partie de votre code PHP, et surtout vos hooks. Pour l'instant, créez un fichier mymodule.module vide.

Résultat

Une fois fait, et après vidage des caches, vous devriez voir votre module en back-office :

Nouveau module en BO

Astuce [D7] Fonctions utiles

String

t($text, $params = array(), $options = array())

Description :

Traduit une chaîne de caractères, avec d'éventuels paramètres. Le troisième argument permet de spécifier un contexte ou une langue.

Exemple :

$text = t('String with @myparam', array('@myparam' => $my_value));

Documentation

Images

image_style_url($style_name, $path)

Description :

Génère L’URL vers le thumbnail d'image correspondant au style d'image en premier argument.

Exemple :

image_style_url(
  'image_thumbnail',
  $node->field_photos[$language->language][$index]['uri']
);

Documentation

URL

l($text, $path, $options = array())

Description :

Crée une balise <a> avec le premier argument comme libellé et le deuxième en href.

Exemple :

image_style_url(
  'image_thumbnail',
  $node->field_photos[$language->language][$index]['uri']
);

Documentation

file_create_url($uri)

Description :

Génère l'URL vers un fichier ou une image, à partir de son uri (de la forme public://mon-image-03.jpg).

Exemple :

file_create_url($node->field_logo[$language->language][0]['uri']);

Documentation

url($path = NULL, $options = array())

Description :

Génère une URL interne ou externe, à partir d'une URL relative ou absolue.

Exemple :

url('node/' . $node->nid)

Documentation

drupal_get_path_alias($path = NULL, $path_language = NULL)

Description :

Retourne l'alias d'URL pour la page avec le chemin en argument

Exemple :

drupal_get_path_alias('node/' . $node->nid)

Documentation

Objets et tableaux

element_children(&$elements, $sort = FALSE)

Description :

Retourne le tableau en entrée, sans les valeurs dont la clé commence par #.

Documentation

Développement

dpm($variable)

Description :

Affiche le contenu de la variable, ses éléments dans le cas d'un tableau, ses attributs dans le cas d'un objet, de manière récursive. (Cette fonction est fournie par le module devel)

Documentation

dpq($select)

Description :

Affiche la requête finale. (Cette fonction est fournie par le module devel)

Documentation

Astuce [D7] Hook après édition/suppression de contenu, multilingue ou non

Drupal fournit 3 hooks pour effectuer des traitements après création/édition/suppression de contenu.

Pour un nœud

À la création

/**
 * Implements hook_node_insert().
 */
function my_module_node_insert($node) {

  // Do something
}

À l'édition

/**
 * Implements hook_node_update().
 */
function my_module_node_update($node) {

  // Do something
}

À la suppression

/**
 * Implements hook_node_delete().
 */
function my_module_node_delete($node) {

  // Do something
}

Pour une entité multilingue

Dans le cadre de contenus (et autres entités) multilingues, 3 autres hooks permettent de connaitre la langue pour laquelle la révision est créée/supprimmée.

La langue utilisée est disponible dans le hook vie la variable $translation['language']. Le type d'entité (ex: user, node, ...) est disponible dans la variable $entity_type.

À la création

/**
 * Implements hook_entity_translation_insert().
 */
function my_module_entity_translation_insert($entity_type, $entity, $translation, $values = array()) {

  // Do something
}

À l'édition

/**
 * Implements hook_entity_translation_update().
 */
function my_module_entity_translation_update($entity_type, $entity, $translation, $values = array()) {

  // Do something
}

À la suppression

/**
 * Implements hook_entity_translation_delete().
 */
function my_module_entity_translation_delete($entity_type, $entity, $langcode) {

  // Do something
}

Astuce [D7] Effectuer des traitements en masse

Pour effectuer des traitements en masse et limiter les problèmes de mémoire, on peut demander à Drupal de les gérer par lots.

Supposons par exemple que l'on souhaite supprimer tous les nœuds de type article, et qu'il y en ait une dizaine de milliers.

// Récupération des nid des nœuds de type article
$results = db_select('node', 'n')
  ->fields('n', array('nid'))
  ->condition('type', 'article', '=')
  ->execute()
  ->fetchCol();

Pour éviter de tout supprimer en une fois, on peut effectuer des suppressions par lots de 500 nœuds.

$nb_op = 500;
$nb_total = count($results);

// Découpage des traitements en lots
foreach (array_chunk($results, $nb_op) as $nids) {
  $operations[] = array('_my_module_batch_delete_nodes', array($nids, $nb_total));
}

// Construction du tableau de paramètre pour le batch
$batch = array(
  'operations' => $operations,
  'title' => t('Delete batch'),
  'init_message' => t('Initializing'),
  'error_message' => t('An error occurred'),
  'finished' => 'my_module_my_custom_end_function'
);

// Exécution du batch
batch_set($batch);
drush_backend_batch_process();

La fonction qui va effectuer la suppression est _my_module_batch_delete_nodes() :

/**
 * Custom batch function to delete multiple nodes.
 *
 * @param $nids Nids of nodes that must be deleted
 * @param $nb_total Number of nodes already deleted
 * @param $context Context to display the progression
 */
function _my_module_batch_delete_nodes($nids, $nb_total, &$context) {

  if (empty($context['results']['progress_d'])) {
    $context['results']['progress_d'] = 0;
  }

  node_delete_multiple($nids);

  // Affichage de la progression
  $context['results']['progress_d'] += count($nids);
  $context['message'] = 'Deleted ' . $context['results']['progress_d'] . '/' . $total;
}

Astuce [D7] Remplir des champs de contenu programmatiquement

Lorsqu'on crée programmatiquement un contenu, voici comment remplir différents types de champ (les lignes ci-dessous sont à ajouter à la place de [...(1)] et/ou [...(2)] dans l'article lié).

Champs multivalués

Dans Drupal, tous les champs sont potentiellement multivalués. Pour ajouter plusieurs valeurs à un même champ, il suffit d'ajouter une nouvelle ligne en incrémentant le [0] dans les lignes ci-dessous.

Par exemple pour un champ texte basique ayant 3 valeurs :

$node->field_text[LANGUAGE_NONE][0]['value'] = 'ma première valeur';
$node->field_text[LANGUAGE_NONE][1]['value'] = 'ma deuxième valeur';
$node->field_text[LANGUAGE_NONE][2]['value'] = 'ma troisième valeur';

Champs multilingues

Dans Drupal, tous les champs sont potentiellement multilingues. Pour ajouter une valeur dans une autre langue, il suffit d'ajouter une ligne en remplaçant LANGUAGE_NONE par une langue.

Exemple avec un champ texte basique et deux langues :

$node->field_text['fr'][0]['value'] = 'ma valeur';
$node->field_text['en'][1]['value'] = 'my value';

Champs texte

Pour remplir un champ texte basique, cette ligne suffit :

$node->field_text[LANGUAGE_NONE][0]['value'] = 'ma valeur';

Si ce champ utilise un format de texte particulier, il faut le préciser. Exemple avec simple_text :

$node->field_text[LANGUAGE_NONE][0]['value'] = 'ma valeur';
$node->field_text[LANGUAGE_NONE][0]['format'] = 'simple_text';

Champs entier/décimal

Même principe pour les champs de type nombre :

$node->field_number[LANGUAGE_NONE][0]['value'] = 42;

Champs booléen

Encore la même chose pour les booléens, $my_boolean étant en fait un entier égal à 0 ou 1 :

$my_boolean = 0;
$node->field_number[LANGUAGE_NONE][0]['value'] = $my_boolean;

Champs image

Pour importer une image programmatiquement vous pouvez utiliser cette fonction :

  /**
   * Copy the image in argument in the drupal upload dir, and return it.
   *
   * @param string $image_path Image path from the root directory
   * @return array an array representing the copied image
   */
  private function copy_image($image_path) {

    $root_dir_path = getcwd();
    $upload_sample_files_uri = file_default_scheme() . '://sample_data'; 

    $file_path = $root_dir_path . $image_path;
    $file = (object) array(
                'uid' => 1,
                'uri' => $file_path,
                'filemime' => file_get_mimetype($file_path),
                'status' => 1,
    );
    $file = file_copy($file, $upload_sample_files_uri);
    return (array) $file;
  }

Pour une image dans un répertoire temp/ à la racine de Drupal ça donne ça :

$node->field_image[LANGUAGE_NONE][0] = copy_image('/temp/mon_image.jpg');

Champs lien

Un champ lien avec une URL, un libellé et d'éventuels attributs HTML :

$node->field_link[LANGUAGE_NONE][0] = array(
  'url' => 'http://www.google.fr',
  'title' => 'Libellé du lien',
  'attributes' => array('title' => 'Contenu de l'attribut HTML title'),  
);

Champs référence entre entités (entityref)

Le champ entityref stocke des id (et donc des nid pour des nœuds) :

$node->field_related_content[LANGUAGE_NONE][0]['target_id'] = $other_node->nid;

Champs adresse

Le champ adresse découpe les adresses en 5 parties :

$node->field_adresse[LANGUAGE_NONE][0] = array(
  'country' => 'FR',
  'locality' => 'Paris',
  'postal_code' => '75000',
  'thoroughfare' => '1, Avenue des Champs Élysées',
  'premise' => '2ème étage',
);

Champs coordonnées - Bounds

Le champ coordonnées permet entre autres de stocker des limites géographiques : des bounds.

$node->field_coordonnees[LANGUAGE_NONE][0]['input_format'] = GEOFIELD_INPUT_BOUNDS;
$node->field_coordonnees[LANGUAGE_NONE][0]['geom']['left'] = '2.320915';
$node->field_coordonnees[LANGUAGE_NONE][0]['geom']['top'] = '48.869911';
$node->field_coordonnees[LANGUAGE_NONE][0]['geom']['right'] = '2.350928';
$node->field_coordonnees[LANGUAGE_NONE][0]['geom']['bottom'] = '48.854086';

Champs métadonnées

Le champ métadonnées ajoutera des balises méta dans le <head> de la page :

$node->metatags[LANGUAGE_NONE] = array(
    'title' => array('value' => 'Contenu de la balise title de la page'),
    'description' => array('value' => 'Contenu de la balise méta description'),
    'abstract' => array('value' => 'Contenu de la balise méta abstract'),
    'keywords' => array('value' => 'Contenu de la balise méta keywords'),
);

D'autres clés peuvent être ajoutées au tableau.

Astuce [D7] Rediriger après la connexion

Pour rediriger l'utilisateur après sa connexion, on peut utiliser le hook_user_login() :

/**
 * Implements hook_user_login().
 */
function my_module_user_login(&$edit, $account) {

    if (!isset($_POST['form_id']) || $_POST['form_id'] != 'user_pass_reset') {

        if (in_array('authenticated user', $account->roles)) {

            // Modification de l'url de destination
            $_GET['destination'] = 'admin/workbench/content/all';
        }
    }
}

L'exemple ci-dessus redirige l'utilisateur vers la page de gestion des contenus du module workbench (admin/workbench/content/all).

Astuce [D7] Trier les résultats d'une requête de manière aléatoire

La fonction orderRandom() permet de trier les résultats d'une requête de manière aléatoire :

db_select('node', 'n')
    ->fields('n', array('nid'))
    ->condition('status', 1)
    ->range(0, 10)
    ->orderRandom();

La requête ci-dessus retourne 10 nœuds aléatoires, à l'état publié.

Astuce [D7] Créer et traduire un contenu programmatiquement

Basiquement, pour créer et publier un contenu, ces lignes suffisent :

$default_language = 'fr';
$node_type = 'article';

// Création d'un contenu vide
$node = new stdClass();
$node->type = $node_type;
$node->uid = 1;
$node->status = 1;
node_object_prepare($node);
$node->language = $default_language;

// Titre du contenu
$node->title = 'Titre de mon contenu'

// Autres champs du contenu
// [...(1)]

// Autres traductions
// [...(2)]

// Sauvegarde du contenu en base
node_save($node);

$nid = $node->nid;

Si le contenu est multilingue et que son titre est traduisible, il faut ajouter la ligne suivante (à la place de [...(1)]) :

$node->title_field[$node->language][0]['value'] = 'Titre de mon contenu';

Pour traduire un ou plusieurs autres champs dans une autre langue que la langue par défaut, il faut ajouter les lignes suivantes (à la place de [...(2)] :

$other_language = 'en';

// Traduction des champs
$node->title_field[$other_language][0]['value'] = 'The title of my content';

// Synchronisation et sauvegarde de la traduction en base
$handler = entity_translation_get_handler('node', $node);
$translation = array(
    'translate' => 0,
    'status' => 1,
    'language' => $other_language,
    'source' => $node->language,
);
$handler->setTranslation($translation, $node);