Drupal ( 20 / 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 [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 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 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).