Planète

Par kgaut
Kevin Gautreau

Drupal 11 - Utiliser la nouvelle syntaxe Orientée Objet pour les Hooks

Drupal 11.1 va introduire une nouvelle façon d'implémenter les hooks, de manière orientée objet !

En pratique il suffira de créer un fichier MesHooks.php (un ou plusieurs fichiers, le nom est libre) dans le dossier src/Hook (cela peut être dans un sous dossier, mais le dossier de base doit être obligatoirement Hook pour être détecté par drupal) de votre module.

Ainsi pour implémenter le hook HOOK_preprocess_node, voici comment cela se passe : 

Je créé un fichier mon_module/src/Hook/NodeHooks.php avec le contenu suivant :

<?php
namespace Drupal\mon_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;

/**
* Provides hook implementations for Node entities.
*/
class NodeHooks {

  /**
   * Prepares the node variables for rendering.
   *
   * @param array $variables
   *   The variables array to preprocess, including the node entity.
   */
  #[Hook('preprocess_node')]
  public static function preprocessNode(&$variables): void {
    $variables['content']['publication_date'] = [
      '#theme' => 'publication_date',
      '#created' => $variables['node']->get('created')->value,
      '#changed' => $variables['node']->get('changed')->value,
    ];
  }
}

c'est l'annotation 

#[Hook('preprocess_node')]

qui indiquera à drupal quel est le hook implémenté par la fonction qui suit (dont le nom est libre)

On peut aussi utiliser une même fonction pour plusieurs hooks : 

  #[Hook('comment_insert')]
  #[Hook('comment_update')]
  public function commentInsertOrUpdate(CommentInterface $comment) {
  ...
  }

La fin du fichier module ?

Presque, comme indiqué dans le change record, tous les hooks ne peuvent pas être implémenté de cette manière là, doivent rester en procédural : 

hook_cache_flush()hook_install()hook_module_preinstall()hook_module_preuninstall()hook_modules_installed()hook_modules_uninstalled()hook_post_update_NAME()hook_requirements()hook_schema()hook_uninstall()hook_update_last_removed()hook_update_N()hook_theme_suggestion_HOOK()hook_theme_suggestions_HOOK_alter()
 Compatibilité avec Drupal 10 et 11.0

Cette syntaxe ne fonctionne qu'à partir de Drupal 11.1, qui devrait sortir en décembre 2024, mais bonne nouvelle, il est possible de l'utiliser dès aujourd'hui en attendant de faire l'upgrade. Par contre dommage, il faut pour cela garder l'implémentation original qui appellera notre classe.

On commence par ajouter notre classe à notre fichier mon_module.services.yml : 

services:
  Drupal\mon_module\Hook\NodeHooks:
    class: Drupal\mon_module\Hook\NodeHooks
    autowire: true

Et on ajoute l'appel à la classe dans mon_module.module : 

/**
* Implements hook_preprocess_node().
*/
#[LegacyHook]
function mon_module_preprocess_node(&$variables) {
  \Drupal::service(Drupal\mon_module\Hook\NodeHooks::class)->preprocessNode($variables);
}

Une fois la migration vers drupal 11.1 faite ces deux ajustements pourront être supprimés.

À noter, pour que les annotations soient connues par Drupal (et que phpstan valide votre code), il est nécessaire d'utiliser le patch présent sur cet issue : https://www.drupal.org/project/drupal/issues/3482464 qui backport les déclarations d'annotations : https://git.drupalcode.org/project/drupal/-/merge_requests/9908.patch

Voir le change record : https://www.drupal.org/node/3442349

Par kgaut
Kevin Gautreau

Drupal + searchApi + SearchStax : résoudre l'erreur « internal server error »

Étrange souci que j'ai rencontré sur un projet Drupal avec searchApi relié à SearchStax.

Sur certaines page j'avais un code retour 500 « internal server error » alors que beaucoup d'autres fonctionnaient sans souci... Pas vraiment de log sur Searchstax pour creuser, ni d'option de debug. Le plus étrange, la même requête dans postman répondait correctement, mais en la faisant via le binaire curl, erreur aussi.

Cela semblait arriver sur les très grosses requêtes, sur une de celles qui posait souci, le body de la requête était proche de 8ko, oui, plus de 8000 caractères.

En finissant par comparer les headers, la solution se trouvait dans le « Accept-Encoding: gzip » présent par défaut dans postman. En l'ajoutant à la requête curl, c'est ok.

J'ai donc utilisé un event pour l'ajouter à l'ensemble des requêtes searchApi :

mon_module/src/EventSubscriber/MonModuleSearchSubscriber.php

<?php
namespace Drupal\mon_module\EventSubscriber;

use Solarium\Core\Event\PostCreateRequest as PostCreateRequestEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* Pop_produit event subscriber.
*/
class MonModuleSearchSubscriber implements EventSubscriberInterface {

  public static function getSubscribedEvents(): array {
    return [
      PostCreateRequestEvent::class => ['postCreateRequestAddHeader'],
    ];
  }

  public function postCreateRequestAddHeader(PostCreateRequestEvent $e) {
    $request = $e->getRequest();
    $request->addHeader('Accept-Encoding: gzip');
  }
}

 

Plus d'erreur, mais on se retrouve maintenant avec une réponse encodée en gzip, il faut la décoder, pas possible d'utiliser un event ici, j'ai donc du faire un patch pour le fichier search_api_solr/Plugin/search_api/backend/SearchApiSolrBackend.php : 

diff --git a/src/Plugin/search_api/backend/SearchApiSolrBackend.php b/src/Plugin/search_api/backend/SearchApiSolrBackend.php
index 752df404..0af49932 100644
--- a/src/Plugin/search_api/backend/SearchApiSolrBackend.php
+++ b/src/Plugin/search_api/backend/SearchApiSolrBackend.php
@@ -1930,6 +1930,9 @@ class SearchApiSolrBackend extends BackendPluginBase implements SolrBackendInter
         // Send search request.
         $response = $connector->search($solarium_query, $this->getCollectionEndpoint($index));
         $body = $response->getBody();
+        if (@gzdecode($body) !== FALSE) {
+          $body = gzdecode($body);
+        }
         if (200 != $response->getStatusCode()) {
           throw new SearchApiSolrException(strip_tags($body), $response->getStatusCode());
         }

et l'ajouter à mon composer.json (voir plus bas pour en savoir plus sur les patchs et composer) : 

        "patches": {
            "drupal/search_api_solr": {
                "Gzip Encoding": "./files/patches/search_api_solr_gzip_encoding.patch"
            }
        }

C'est pas fou, il y a peut-être une solution plus propre, mais j'espère que ça aidera quelqu'un car les ressources sur le sujet semblent inexistantes. La page searchstax concernant ce code d'erreur ne donne aucune piste pointant sur ce genre de solution.

Merci Romain pour m'avoir mis sur la bonne piste !

 

Par Simon Georges
Simon Georges
Drupal depuis plus de 10 ans, SEO depuis 3 ans

Migration d'un site Drupal 7 en Drupal 10

Trucs, astuces et "bouts" de code pour migrer votre site web de Drupal 7 à Drupal 10. Compte-rendu d'une conférence donnée au Drupalcamp Rennes 2024.

Par kgaut
Kevin Gautreau

Drupalcamp 2024 : Une pipeline de déploiement aux petits oignons avec Gitlab CI

Les diapositives de la présentation : https://docs.google.com/presentation/d/1i0FZDAzQDJAidGSiyxdFbDD7oeaVh7Q…

Dépôt test / démo CI :  https://gitlab.com/kgaut/drupal.kgaut.xyz

Mon dépôt de job CI : https://gitlab.com/kgaut/gitlab-ci-templates et sa documentation : https://gitlab-ci-templates.kgaut.net/

 

Par kgaut
Kevin Gautreau

Édito du 27 mars 2024 : (rail)road to drupalcamp Rennes 2024 !

En direct d'un train qui circule avec 50 minutes de retard, je devrais quand même arriver dans la journée à Rennes, où commence, demain, le Drupalcamp 2024, organisée par la formidable équipe de bénévoles et l'association Drupal France et Francophonie.

Ma présentation est quasiment terminée, j'y parlerai le jeudi de CI avec Drupal et comment utiliser au mieux Gitlab CI pour l'ensemble de ses projets web. C'est le jeudi 28 mars 2024 à 16H45 dans l'auditorium. 

La présentation sera capté, vélo-typée, je partagerai tout ça ici en temps et en heure. 

Ça faisait depuis le drupalcamp de Paris en 2019 que je n'avais pas participé à un évènement drupal, j'ai hâte de revoir vos têtes, et n'hésitez-pas à me faire un petit coucou si vous m'y croisez et que je vous snobe car je suis très mauvais me souvenir des têtes.

Par kgaut
Kevin Gautreau

Drupal - Ajouter des champs au formulaire « Paramètres de base du site »

Voici comment ajouter un ou plusieurs champs au formulaire de configuration « paramètres de base du site ».

On commence par créer un service qui nous permettra de surcharger le formulaire de base : 

modules/mon_module/mon_module.services: 

services:
  mon_module.route_subscriber:
    class: Drupal\mon_module\Routing\RouteSubscriber
    tags:
      - { name: event_subscriber }

 

modules/mon_module/src/Routing/RouteSubscriber.php

<?php
namespace Drupal\mon_module\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
use Drupal\mon_module\Form\BasicSiteInformationForm;
class RouteSubscriber extends RouteSubscriberBase {
  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    if ($route = $collection->get('system.site_information_settings'))
      $route->setDefault('_form', BasicSiteInformationForm::class);
  }
}

Et la surcharge du formulaire :

modules/mon_module/src/Form/BasicSiteInformationForm.php

<?php

namespace Drupal\mon_module\Form;

use Drupal\Core\Form\FormStateInterface;
use Drupal\system\Form\SiteInformationForm;


class BasicSiteInformationForm extends SiteInformationForm {

  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('system.site');
    $form = parent::buildForm($form, $form_state);
    // On ajoute nos champs
    $form['site_information']['email_service_commercial'] = [
      '#type' => 'email',
      '#title' => t('Adresse email Service Commercial'),
      '#default_value' => $config->get('email_service_commercial') ?? '',
      '#description' => t("Adresse à laquelle sera envoyée les notifications de création de compte"),
      '#required' => TRUE,
    ];
    return $form;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Et on les enregistre
    $this->config('system.site')
      ->set('email_service_commercial', $form_state->getValue('email_service_commercial'))
      ->save();
    parent::submitForm($form, $form_state);
  }
}

Et voila : 

Image
Formulaire paramètres de base du site

Par Simon Georges
Simon Georges
Drupal depuis plus de 10 ans, SEO depuis 3 ans

Plateforme web Drupal de la Communauté Employeur Pro-Vélo

Le contexte

La FUB, Fédé­­ra­­tion Française des Usagers de la Bicy­­clette, anime un programme visant à accom­pa­gner les employeurs dans la promo­tion du vélo pour les dépla­ce­ments domi­cile-travail

Par Simon Georges
Simon Georges
Drupal depuis plus de 10 ans, SEO depuis 3 ans

Makina Corpus, partenaire de la DrupalCamp 2024

Nous sommes fiers d’an­non­cer que Makina Corpus est le spon­sor de la Drupal­Camp à Rennes. Notre expert vous y propose une confé­rence « migrer de Drupal 7 à Drupal 10 ».

Par kgaut
Kevin Gautreau

Drupal 10 - Créer un Event, le lancer et l'intercepter

Version actualisée de Drupal 8 - Créer un Event, le lancer et l'intercepter pour drupal 10+

Nous allons voir ici comment créer un Event, le lancer et l'intercepter.

Cet évènement sera lancé lorsqu'un utilisateur se connecte au site.

Création de l'event

Dans modules/monmodule/src/Event/UserLoginEvent.php

<?php

namespace Drupal\monmodule\Event;

use Drupal\user\UserInterface;
use Drupal\Component\EventDispatcher\Event;

/**
* Event that is fired when a user logs in.
*/
class UserLoginEvent extends Event {

  const EVENT_NAME = 'kgaut_tools_user_login';

  /**
   * The user account.
   *
   * @var \Drupal\user\UserInterface
   */
  public $account;

  /**
   * Constructs the object.
   *
   * @param \Drupal\user\UserInterface $account
   *   The account of the user logged in.
   */
  public function __construct(UserInterface $account) {
    $this->account = $account;
  }
}

Lancement de l'Event (dispatcher)

Dans mon cas en particulier, je dois répondre lorsqu'un utilisateur se connecte, je vais donc utiliser le hook HOOK_user_login.

Dans modules/monmodule/monmodule.module

function monmodule_user_login($account) {
  $event = new \Drupal\monmodule\Event\UserLoginEvent($account);
  /** @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $event_dispatcher */
  $event_dispatcher = \Drupal::service('event_dispatcher');
  $event_dispatcher->dispatch($event, $event::EVENT_NAME);
}

Interception de l'évènement (Subscriber)

Nous allons pour cela créer un EventSubscriber.

Dans modules/monmodule/src/EventSubscriber/UserLoginSubscriber.php

<?php

namespace Drupal\monmodule\EventSubscriber;

use Drupal\monmodule\Event\UserLoginEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class UserLoginSubscriber implements EventSubscriberInterface {

  public static function getSubscribedEvents() {
    // Ici on défini pour chaque évènement, la méthode à utiliser
    return [UserLoginEvent::EVENT_NAME => 'UserLoginCallback'];
  }

  public function UserLoginCallback(UserLoginEvent $event) {
    $account = $event->account;
    //Ici faire le traitement voulu
  }

}

Note : Pour cette dernière partie, il est aussi possible d'utiliser la commande drush :

drush generate service:event-subscriber
Par kgaut
Kevin Gautreau

Drupal - Utiliser une autre base de données pour certaines tables

Les raisons peuvent être diverses, mais parfois on veut que certaines tables soient dans une base de données spécifique au lieu de la principale. 

Dans mon cas c'est lors d'un déploiement « bleu / vert ». 

J'ai deux bases de données : prod_a et prod_b, la prod_a est la base active. 

Voici le processus de mise en prod simplifié

La base de données de staging est copiée sur la base de prod non active : prod_b.Une copie de certaines tables (watchdog, inscrits_newsletter...) et faite depuis la base prod_a vers prod_b.Bascule entre les deux base de prod, la prod_b devient la base active et la prod_a ne sert plus.

dans les faits, cela fonctionne, mais l'étape 2 peut-être longue. Pour gagner du temps on peut mettre les tables qui ne doivent pas être écrasées sur une troisième base que l'on appellera prod_common.

voici la définition classique d'une base de données drupal :

$databases['default']['default'] = [
  'database' => 'prod',
  'username' => 'user',
  'password' => 'password',
  'prefix' => '',
  'host' => '',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
];

nous allons définir maintenant une seconde base prod_common 

$databases['default']['common'] = [
  'database' => 'prod_common',
  'username' => 'user',
  'password' => 'password',
  'prefix' => '',
  'host' => '',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
];

Notez bien le changement de clé dans le nom de la base :  $databases['default']['common']

Maintenant pour spécifier quelles tables doivent être dans quel schema, nous allons utiliser l'attribut prefix de notre définition, voici la version complète : 

$databases['default']['default'] = [
  'database' => 'prod',
  'username' => 'user',
  'password' => 'password',
  'prefix' => [
    'default' => '', // par défaut, tout doit être dans le schéma par défaut
    'watchdog' => 'prod_common.', // la table watchdog va elle dans la base prod_common (attention au point)
    'inscrits_newsletter' => 'prod_common.', // idem pour la table inscrits_newsletter
  ],
  'host' => '',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
];

// Je duplique la définition de ma base prod en modifiant uniquement le nom de la base de données
$databases['default']['common'] = $databases['default']['default'];
$databases['default']['common']['database'] = 'prod_common';

Et voila, pour les deux tables spécifiées, drupal ira chercher et enregistrer les informations sur la base prod_common.

Par kgaut
Kevin Gautreau

Drupal - Créer son premier service

En anglais, une bonne ressource pour créer son premier service avec Drupal.

Une fois le principe compris, il existe aussi la commande 

drush generate service:custom
Par kgaut
Kevin Gautreau

Pourquoi faire la mise à jour drupal 10 ?

Vous avez un site internet Drupal en version 8 ou 9, votre prestataire vous propose une mise à jour en drupal 10 alors que votre site marche très bien ? Nous allons voir pourquoi cela reste une bonne idée.

Note : ce post est rédigé à destination de plusieurs agences avec lesquelles je travaille afin de fournir un argumentaire pour pousser cette montée de version aux clients finaux.

La sécurité

La dernière version de Drupal 8 (8.9.20) est arrivée en fin de vie en novembre 2021.

La dernière version de Drupal 9 (9.5.11) est arrivée en fin de vie en novembre 2023.

Drupal 7 lui a vu sa date de fin de vie plusieurs fois repoussée, maintenant fixée à janvier 2025.

C'est-à-dire que ces versions ne reçoivent plus de mise à jour, et si une faille de sécurité est découverte, en théorie elle ne sera pas corrigée. Dans les faits, lors de certaines grosses failles impactant le core de drupal, des patchs avaient été créés même pour les versions théoriquement non supportées. Mais rien ne dit que ce sera le cas à l’avenir.

La version actuelle de drupal (10.2.2) sera supportée jusqu’à décembre 2024. Cela peut sembler peu, mais depuis Drupal 8, les mises à jour de version intermédiaires (de 10.1.x à 10.2.x) sont grandement simplifiées. En suivant ces mises à jour régulièrement, une version intermédiaire sortant tous les 6 mois, la date de fin de vie est repoussée d’autant.

Les mises à jour de versions majeures (9.x à 10.x par exemple) demandent un peu plus de temps, mais n’ont rien à voir avec une mise à jour depuis la version 7 vers une version supérieure, qui nécessite une réécriture du code ainsi qu’un travail de migration du contenu.

Au-delà de cette migration « one shot », il est stratégique de prévoir avec votre prestataire un accompagnement régulier pour maintenir le core de drupal ainsi que les modules à jour, de faire les mises à jour intermédiaires (10.1.x vers 10.2.x par exemple) régulièrement. Ainsi vous avez l’assurance d’avoir un site supporté au niveau de la sécurité, et que les mises à jour majeures (10.x vers la future 11.x) soient le plus indolore possible.

Permettre l’évolutivité de votre site

L’autre gros avantage de mettre son site à jour et de le garder maintenable et permettre les évolutions. Aujourd’hui rares sont les modules drupal contrib qui sorte une nouvelle version encore compatible avec drupal 9, encore moins avec drupal 8. 

Vous risquez ainsi de vous retrouver rapidement bloqué et que toute nouvelle fonctionnalité nécessite un développement spécifique qui sera plus long que l’activation et la configuration d’un module existant, donc plus cher. 

Même pour du développement spécifique, à chaque nouvelle version de drupal, son API évolue et propose de nouvelles fonctionnalités pour le code et le theming.

Les prérequis évoluent aussi, ainsi pour drupal 10 la version minimale de php est la 8.1, qui apporte là aussi de nouvelles fonctions et améliorations de performances.

Profiter de nouvelles fonctionnalités

Le code change mais l'expérience d’administration évolue aussi, à chaque nouvelle version de drupal, des modules core sont améliorés, certains remplacés par d’autres. 

On pourra notamment citer dans les nouveautés de drupal 10 : 

Le remplacement de l’éditeur de texte Ckeditor 4 par Ckeditor 5

Image
Ckeditor 5

Le changement du thème d’administration : claro (en remplacement de Seven qui date de drupal 7, comme son nom l’indique)

Image
Thème BO claro

Un nouveau concept de « workspace », qui permet d’avoir plusieurs versions de son site avec les différentes révisions de contenu. Une version « Prod » pour ce qui est visible pour les internautes, une version « Staging » pour le contenu en validation.

Combien ça va me couter ?

Ça dépend. 

Oui évidemment. 

Là ou une mise à jour de drupal 7 vers drupal 10 peut-être considérée comme une refonte complète et nécessite une réécriture complète du code, les mises à jour depuis drupal 8 vers drupal 10 sont elles plus simples. le framework bas niveau reste le même. Il faudra juste à votre prestataire de faire les mises à jour des modules « contrib » (les modules communautaires), adapter le code « custom » (code métier, développé spécifiquement pour votre site) en corrigeant les fonctions dépréciées… 

Comme je l’ai dis plus tôt, une montée de version de drupal entraîne aussi généralement une montée de version de PHP, qui peut là aussi nécessiter des modifications du code custom pour la compatibilité.

Suivi donc la complexité du code custom, l’utilisation de modules contrib un peu exotiques, une montée de version de drupal 9 à drupal 10 peut nécessiter d’une demie journée à 2-3 jours, voir plus sur certains projets très complexes.

Des outils pour aider dans cette migration

Il existe pour les développeurs des outils pour gagner du temps lors de cette migration : 

Upgrade Status

 Upgrade

Module permettant d'avoir un tableau de bord pour préparer la mise à jour de son site drupal, que ça soit de drupal 8 à drupal 9 ou de drupal 9 à drupal 10.

Il liste les modules tiers (contrib) qui sont à mettre à jour, permet de scanner les modules et thèmes custom pour signaler les appels de fonctions dépréciés. 

Lire la suite de Upgrade Status

Rector

 Upgrade

Scan votre code custom et remplace automatiquement les appels de fonction dépréciés par leur remplaçants.

Cela fonctionne à la fois pour Drupal lors d'une montée de version, mais aussi pour les montées de version de PHP.

Lire la suite de Rector

Par kgaut
Kevin Gautreau

Drupal - Nouveau module : Database Dashboard

Il y a quelques semaines, souci rencontré sur un projet client avec une base de données qui grossissait de manière anormale. Ne gérant pas l'hébergement pour ce projet, je n'ai eu aucune alerte, avant que le site plante car le serveur de base de données n'avait plus d'espace disque disponible.

Quelques requêtes plus tard nous avons pu identifier les tables fautives et donc les causes.

Afin de surveiller ça de manière basique, j'ai alors développé un petit module drupal qui affiche les tables avec le plus de lignes et les tables qui prennent le plus d'espace disque. Et la même chose pour les tables de cache au passage.

Image
Tableau de bord Base de données

J'ai pris le temps de packager le module sur drupal.org, car je me suis dis qu'il pourrait être utile à d'autres (et à moi pour d'autres projets).

La page drupal.org est la suivante : https://www.drupal.org/project/database_dashboard

Installation

Comme n'importe quel module drupal : 

composer require drupal/database_dashboard

Il vous faudra modifier le fichier settings.php ou settings.local.php et ajouter les lignes suivantes à la fin :

$databases['schema']['default'] = [
  'host' => 'mariadb', // À adapter
  'database' => 'information_schema',
  'username' => 'drupal', // À adapter
  'password' => 'drupal', // À adapter
  'prefix' => '',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
];

Activez le module via le backoffice ou via drush.

Ensuite rendez-vous sur /admin/reports/database ou via le menu d'administration : Rapports / Database Dashboard afin de consulter la page.

Rien de révolutionnaire, mais ça peut servir. 

N'hésitez-pas si vous avez des idées d'indicateurs à ajouter !

 

Pages