Planète

Par KarimB
Karim Boudjema
Je suis Karim Boudjema ou KarimB en ligne. Je suis belge, développeur Drupal et aussi administrateur d'entreprises. Je vis pour l'instant à Cochabamba, en Bolivie. Quand j'ai découvert Drupal 4.7 en 2008 (et oui… il y a 10 ans), j'ai tout suite senti que ce serait un changement important dans le monde du développement web. Et ce fut le cas!

Créer une queue avec un contrôleur sous Drupal 8

Sous Drupal, les queues sont particulièrement importantes lorsque nous avons besoin de postposer certaines tâches pour un traitement futur. Pour ce faire, nous allons placer ces tâches ou données dans une queue (créer la queue) et ensuite nous allons traiter ces données avec un 'QueueWorker plugin' (traitement de la queue), généralement grâce à un process de type cron.

Il existe plusieurs manières de créer une queue:
- Avec un formulaire
- Avec un contrôleur
- Avec une fonction hook_cron()
 
Pour traiter la queue, nous avons aussi différentes options:
- Par cron avec un 'QueueWorker plugin'
- Par un batch process aussi avec un 'QueueWorker plugin' mais en étendant un plugin de base
- Par un batch process qui traitera chaque élément de la queue dans un service ou dans un contrôleur.

Par KarimB
Karim Boudjema
Je suis Karim Boudjema ou KarimB en ligne. Je suis belge, développeur Drupal et aussi administrateur d'entreprises. Je vis pour l'instant à Cochabamba, en Bolivie. Quand j'ai découvert Drupal 4.7 en 2008 (et oui… il y a 10 ans), j'ai tout suite senti que ce serait un changement important dans le monde du développement web. Et ce fut le cas!

Accélérer composer avec Drupal 8

Lancer un composer install ou un composer update dans notre installation Drupal 8, pour installer ou actualiser des modules ou des themes, peut parfois être assez frustrant car Composer est vraiment lent. Trop lent en fait. Voici quelques tips qui vous permettront d'accélérer Composer en travaillant avec Drupal.

Installer Prestissimo

Prestismo est un plugin global de Composer qui permet de réaliser des installations (connections) en parallèle et il est vraiment très rapide. Il peut être 2x plus rapide. Mais Prestissimo a besoin de cURL, qui parfois ne fonctionne pas derrière certains firewalls ou proxies.

Pour installer Prestissimo suivez les étapes suivantes:

composer self-update

composer global require hirak/prestissimo

Une fois Prestissimo installé, Composer devrait être bien plus rapide qu'auparavant.

Vous pouvez configurer le nombre de connections parallèles que vous désirez, mais il semble que les 6 connections simultanées par défaut fonctionnent assez bien.

Par KarimB
Karim Boudjema
Je suis Karim Boudjema ou KarimB en ligne. Je suis belge, développeur Drupal et aussi administrateur d'entreprises. Je vis pour l'instant à Cochabamba, en Bolivie. Quand j'ai découvert Drupal 4.7 en 2008 (et oui… il y a 10 ans), j'ai tout suite senti que ce serait un changement important dans le monde du développement web. Et ce fut le cas!

Installer Drupal 8 avec composer

Aujourd'hui composer semble s'imposer comme la manière 'recommandée' pour installer (ou plus précisément de télécharger) Drupal 8, tant pour le cœur de Drupal mais aussi pour les modules comme pour les thèmes.

Donc, pour commencer un nouveau projet sous Drupal 8, nous devrons télécharger le code avec composer et non plus avec Drush ou Drupal Console comme nous le faisions dans le passé. Bien entendu, nous pouvons toujours utiliser Drush ou DC pour activer (ou installer) les modules ou thèmes contrib, mais pas pour les télécharger.

Le principal atout de composer est de pouvoir administrer une liste toujours plus étendue de dépendances (comme de leurs sous-dépendances) et ainsi de s'assurer que les bonnes versions de chaque 'package' soient utilisées ou actualisées.

Par Christophe MOLLET
Christophe Mollet

Développer un thème sur Drupal 8

Cet article vous propose sous forme d'un tutoriel les étapes de la création d'un thème "from scratch" sous Drupal 8. En plus de reprendre les bonnes pratiques lors du développement d'un thème, il explique précisément comment désactiver les caches pour faciliter la phase de développement.

Si des connaissances sur le "back-office" de Drupal et dans les langages de type "html, css et twig" sont nécessaires, ce tutoriel se veut accessible pour les personnes désirant aller plus loin dans la personnalisation de leur site Drupal.

Par Kgaut
Adhérent
Kevin Gautreau

Drupal 8 - Supprimer toutes les entités d'un certain type

Dans mon_module.install :

  1. /**
  2.  * Remove all products
  3.  */
  4. function mon_module_update_8001() {
  5. $toDelete = \Drupal::entityQuery('product')->execute();
  6. foreach ($toDelete as $item) {
  7. $item = \Drupal\clearblue\Entity\Product::load($item);
  8. $item->delete();
  9. }
  10. }

 

Par Kgaut
Adhérent
Kevin Gautreau

Drupal 8 - Supprimer un type d'entité

Dans mon_module.install :

  1. /**
  2.  * Remove Product entity type
  3.  */
  4. function mon_module_update_8001() {
  5. $entity_type = 'product';
  6. $entity_update_manager = \Drupal::entityDefinitionUpdateManager();
  7. $entity_type = $entity_update_manager->getEntityType(entity_type);
  8. $entity_update_manager->uninstallEntityType($entity_type);
  9. }

 

Par Kgaut
Adhérent
Kevin Gautreau

Drupal 8 - Ajouter une propriété à un type d'entité config

Voici comment altérer un type d'entité config pour lui ajouter une propriété.

Dans le cas présent, nous allons modifier le type d'entité ConfigurableLanguage afin de lui ajouter un champ google_analytics, pour avoir par exemple un code de tracking différent pour chaque langue.

Commençons par définir les nouveaux formulaire concernant pour l'édition et la création de langue :

modules/custom/mon_module/mon_module.module

  1. function mon_module_entity_type_alter(array &$entity_types) {
  2. $entity_types['configurable_language']->setFormClass('add', \Drupal\mon_module\Entity\Form\LanguageCustomAddForm::class);
  3. $entity_types['configurable_language']->setFormClass('edit', \Drupal\mon_module\Entity\Form\LanguageCustomEditForm::class);
  4. }

 

Ci dessous, le contenu des formulaires qui héritent directement des formulaires de base qu'ils supplantent.

modules/custom/mon_module/src/Entity/Form/LanguageCustomAddForm.php

  1.  
  2. namespace Drupal\mon_module\Entity\Form;
  3.  
  4. use Drupal\Core\Form\FormStateInterface;
  5. use Drupal\language\Form\LanguageAddForm;
  6.  
  7. class LanguageCustomAddForm extends LanguageAddForm {
  8. use LanguageCustomTrait;
  9.  
  10. public function form(array $form, FormStateInterface $form_state) {
  11. $form = parent::form($form, $form_state);
  12. $this->getFormCustomFields($form);
  13. return $form;
  14. }
  15.  
  16. }

 

modules/custom/mon_module/src/Entity/Form/LanguageCustomEditForm.php

  1.  
  2. namespace Drupal\mon_module\Entity\Form;
  3.  
  4. use Drupal\Core\Form\FormStateInterface;
  5. use Drupal\language\Form\LanguageEditForm;
  6.  
  7. class LanguageCustomEditForm extends LanguageEditForm {
  8. use LanguageCustomTrait;
  9.  
  10. public function form(array $form, FormStateInterface $form_state) {
  11. $form = parent::form($form, $form_state);
  12. $this->getFormCustomFields($form);
  13. return $form;
  14. }
  15.  
  16. }

 

Enfin, afin d'éviter la répétition de code, je passe par un trait qui contient mes customisations.

Ce trait est appelé dans les classes ci-dessous via l'appel : $this->getFormCustomFields($form);

Ce trait, fait deux choses, dans la méthode getFormCustomFields() il altère le formulaire pour ajouter le champs qui nous intéresse, et via l'appel à la méthode customEntityBuilder(), l'enregistrement de ce champ est effectué.

modules/custom/mon_module/src/Entity/Form/LanguageCustomTrait.php

  1.  
  2. namespace Drupal\mon_module\Entity\Form;
  3.  
  4. use Drupal\Core\Form\FormStateInterface;
  5. use Drupal\language\ConfigurableLanguageInterface;
  6.  
  7. trait LanguageCustomTrait {
  8.  
  9. public function getFormCustomFields(&$form) {
  10. /* @var $language \Drupal\language\ConfigurableLanguageInterface */
  11. $language = $this->entity;
  12.  
  13. $form['google_analytics'] = [
  14. '#title' => t('ID Google Analytics'),
  15. '#type' => 'textfield',
  16. '#default_value' => $language->getThirdPartySetting('mon_module', 'google_analytics')
  17. ];
  18. $form['#entity_builders'][] = '::customEntityBuilder';
  19. }
  20.  
  21. function customEntityBuilder($entity_type, ConfigurableLanguageInterface $language, &$form, FormStateInterface $form_state) {
  22. if ($form_state->getValue('google_analytics')) {
  23. $language->setThirdPartySetting('mon_module', 'google_analytics', $form_state->getValue('google_analytics'));
  24. return;
  25. }
  26. $language->unsetThirdPartySetting('mon_module', 'google_analytics');
  27. }
  28.  
  29. }

évidement, pensez à modifier toutes les occurrences de mon_module par le nom machine de votre module.

Merci à Alexandre Mallet aka @woprrr pour la piste

Par Kgaut
Adhérent
Kevin Gautreau

Drupal 8 - Afficher un noeud via le code

Pour afficher avoir le renderable array d'un noeud dans le code, rien de plus simple via le service entity_type.manager :

  1. $node = Node::load(1);
  2. $node_rederable = \Drupal::service('entity_type.manager')->getViewBuilder('node')->view($node);

Pour utiliser un autre view_mode :

  1. $node = Node::load(1);
  2. $node_rederable = \Drupal::service('entity_type.manager')->getViewBuilder('node')->view($node, 'embed');

Évidement le plus propre et de passer par l'injection de dépendance, voici une version simplifiée de mon contrôleur :

  1.  
  2. namespace Drupal\mon_module\Controller;
  3.  
  4. use Drupal\Core\Controller\ControllerBase;
  5. use Drupal\Core\Entity\EntityTypeManager;
  6. use Drupal\node\Entity\Node;
  7. use Symfony\Component\DependencyInjection\ContainerInterface;
  8.  
  9. /**
  10.  * Class PageController.
  11.  */
  12. class PageController extends ControllerBase {
  13.  
  14. /**
  15.   * @var \Drupal\Core\Entity\EntityTypeManager
  16.   */
  17. protected $entityTypeManager;
  18.  
  19. public function __construct(EntityTypeManager $entityTypeManager) {
  20. $this->entityTypeManager = $entityTypeManager;
  21. }
  22.  
  23. public static function create(ContainerInterface $container) {
  24. return new static($container->get('entity_type.manager'));
  25. }
  26.  
  27. public function frontpage() {
  28. $node = Node::load(1);
  29. $page = $this->entityTypeManager->getViewBuilder('node')->view($node, 'embed');
  30.  
  31. return [
  32. 'page' => $page,
  33. ];
  34. }
  35. }

 

Par Kgaut
Adhérent
Kevin Gautreau

Drupal 8 - Afficher un menu dans le code

Dans le contrôleur / Bloc...

  1. /** @var \Drupal\Core\Menu\MenuLinkTree $menu_tree_service */
  2. $menu_tree_service = \Drupal::service('menu.link_tree');
  3. $menu_parameters = new \Drupal\Core\Menu\MenuTreeParameters();
  4. $menu_parameters->setMaxDepth(1); // Profondeur du menu à afficher
  5. $menu_name = 'footer' // Nom machine du menu à afficher
  6. $menus = [
  7. 'footer' => $menu_tree_service->build($menu_tree_service->load($menu_name, $menu_parameters)),
  8. ];
  9. return [
  10. '#theme' => 'page-404',
  11. '#menus' => $menus,
  12. ];

et tout simplement dans notre template :

<span class="br0">{</span><span class="br0">{</span> <span class="re0">menus</span><span class="re1">.footer</span> <span class="br0">}</span><span class="br0">}</span>

 

Par Kgaut
Adhérent
Kevin Gautreau

Drupal 8 - Créer la traduction d'une chaine de caractère dans le code

Dans un processus de déploiement, il est utile de gérer les traductions de chaînes de caractères dans le code, afin de pouvoir les déployer plus facilement.

Exemple avec cette fonction d'update à adapter en fonction de vos besoins :

  1. /**
  2.  * Création d'une traduction
  3.  */
  4. function mespronos_tweaks_update_8005() {
  5. $chaine = 'Forgotten password';
  6. $traduction = 'Mot de passe oublié';
  7. $storage = \Drupal::service('locale.storage');
  8. $string = $storage->findString(['source' => $chaine]);
  9. if ($string === NULL) {
  10. $string = new \Drupal\locale\SourceString();
  11. $string->setString($chaine);
  12. $string->setStorage($storage);
  13. $string->save();
  14. }
  15.  
  16. $translation = $storage->createTranslation(array(
  17. 'lid' => $string->lid,
  18. 'language' => 'fr',
  19. 'translation' => $traduction,
  20. ))->save();
  21. }

Pour un exemple réel, il sera plus pratique de passer par un tableau associatif (à deux dimensions si l'on veut importer plusieurs langues) qui sera parcouru par un ou deux foreach.

Par Kgaut
Adhérent
Kevin Gautreau

Drupal 8 - Ajouter une restriction par ip sur une route

Dans le fichier MODULE.routing.yml on va utiliser le requirement « _custom_access »

  1. module.ma_methode:
  2.   path: 'mon-module/mon-chemin'
  3.   defaults:
  4.   _controller: '\Drupal\module\Controller\monController::maMethode'
  5.   requirements:
  6.   _custom_access: '\Drupal\module\Controller\monController::maMethodeAccess'

Que l'on va implémenter dans notre contrôleur, ici monController.php :

  1. public function maMethodeAccess() {
  2. /** @var Request $request */
  3. $request = \Drupal::request();
  4. $ipAutorisees = \Drupal::config('iamyourstory.checkcard_api')->get('allowed_ips')
  5. $ipAutorisees = explode(',', $ipAutorisees);
  6. $ipAutorisees = array_map('trim', $ipAutorisees);
  7. return AccessResult::allowedIf(\in_array($request->server->get('REMOTE_ADDR'), $ipAutorisees));
  8. }

à noter :

  • Les ip autorisées sont stockées en configuration via un formulaire de config, les ip sont séparées par une virgule, d'où le explode.
  • J'utilise ensuite la fonction array_map('trim', $ipAutorisees) pour supprimer les éventuels espaces. Si la personne avait saisi « 127.0.0.1, 192.168.0.2 », mon tableau sans le trim sera ['127.0.0.1', ' 192.168.0.2'] (espace au départ de la seconde ip.)
Par Kgaut
Adhérent
Kevin Gautreau

Drupal 8 - Entité - Champ de base Fichier (File)

Voici comment créer un champ « fichier » au sein d'un type d'entité custom dans drupal 8 :

  1. $fields['programme_pdf'] = BaseFieldDefinition::create('file')
  2. ->setLabel(t('Programme PDF'))
  3. ->setSetting('file_directory', 'formations/programme') // dossier d'upload
  4. ->setSetting('max_filesize', '10MB') // taille max du fichier
  5. ->setSetting('file_extensions', 'pdf') // extensions autorisées, à séparer par un espace
  6. ->setSetting('description_field', FALSE) // si on veut activer un champ « description »
  7. ->setDisplayOptions('form', [
  8. 'label' => 'hidden',
  9. 'type' => 'file_generic',
  10. 'weight' => 4,
  11. ])
  12. ->setDisplayConfigurable('form', TRUE)
  13. ->setDisplayConfigurable('view', TRUE);

 

Par Kgaut
Adhérent
Kevin Gautreau

Drupal 8 - Entitée & Views - Créer une relations inverse

Prenons deux types d'entités custom : « Bière » et « Brasserie » avec une relation 1-n entre les deux dans le sens :

  • Une bière provient d'une et d'une seule brasserie
  • Une brasserie peut proposer N bières

 Ainsi :

drupal-views-relations.png

En drupalisme, on aurait une propriété « entity_reference » au niveau de notre bière qui fera référence à la brasserie.

Dans views, si on fait un listing des bières, pas de soucis pour accéder au contenu de la brasserie depuis la bière, par contre l'inverse n'est pas possible.

Depuis un listing de brasserie, il n'est pas possible d’accéder aux bières de la brasserie.

Pour cela il faut utiliser la classe en charge de views_data, définie dans l'annotation de notre type d'entité brasserie :

drupal-entitee-annotation.png

Et voila le contenu de ce fichier

  1.  
  2. namespace Drupal\mon_module\Entity\ViewsData;
  3.  
  4. use Drupal\views\EntityViewsData;
  5.  
  6. class BrasserieViewsData extends EntityViewsData {
  7.  
  8. /**
  9.   * {@inheritdoc}
  10.   */
  11. public function getViewsData() {
  12. $data = parent::getViewsData();
  13. $data['brasserie']['bieres'] = [
  14. 'title' => t('Bieres'),
  15. 'help' => t('Lie la brasserie aux bières produites'),
  16. 'relationship' => [
  17. 'group' => t('bieres'), // Affiché en information dans la partie « relationship » de views
  18. 'label' => t('Actions de formation'), // Affiché en information dans la partie « relationship » de views
  19. 'base' => 'biere', // Table de base de l'entitée cible
  20. 'field table' => 'biere', // Table contenant le champ de l'entitée cible sur lequel on fera la jointure
  21. 'base field' => 'brasserie',// Champ de l'entité cible Champ sur lequel on fera la jointure
  22. 'relationship field' => 'brasserie_id', // Champ de l'entité source sur lequel on fera la jointure
  23. 'id' => 'standard',
  24. ],
  25. ];
  26. return $data;
  27. }
  28. }

Et voila le travail !

views-relationship.png

Par Kgaut
Adhérent
Kevin Gautreau

Drupal - PSA-2018-001 - Patch de sécurité déployé le 28/03/2018

Mise à jour du 29 mars 2018 :

Patchez votre site

Si vous n'avez pas encore patché / mis à jour votre site, faite le ! Téléchargement des nouvelles versions ou des patchs à cette adresse : https://www.drupal.org/sa-core-2018-002.

Si vous utilisez composer, la commande pour mettre à jour le core est :

  1. composer update drupal/core

Si vous utilisez drush

  1. drush rf
  2. drush up

La faille

Il s'agit de variables get / post / cookies / pouvant contenir des « # » dans leurs noms et potentiellement déclencher des injections sql ? Je n'ai pas encore vu d'exploit mais les implications données sur la FAQ de la faille sont violente :

 

  • How difficult is it for the attacker to leverage the vulnerability? None (user visits page).
  • What privilege level is required for an exploit to be successful? None (all/anonymous users).
  • Does this vulnerability cause non-public data to be accessible? All non-public data is accessible.
  • Can this exploit allow system data (or data handled by the system) to be compromised? All data can be modified or deleted.
  • Does a known exploit exist? Theoretical or white-hat (no public exploit code or documentation on development exists)
  • What percentage of users are affected? Default or common module configurations are exploitable, but a config change can disable the exploit.

Source : https://groups.drupal.org/security/faq-2018-002

En tout cas le patch permet de filtrer ces tableaux et enlever toutes les éléments pouvant être dangereux.

Il est possible d'avoir une whitelist des paramètres à ne pas supprimer dans le cas où vous utilisez des noms de cookies, paramètres GET ou POST commençant par des « # ».

Pour cela, sous drupal 7, dans le fichier settings.php

  1. $conf['sanitize_input_whitelist'] = ['#clee_1', '#clee_2'];

Sous drupal 8, dans le fichier settings.php aussi :

  1. $settings['sanitize_input_whitelist'] = ['#clee_1', '#clee_2'];

Il y a aussi un système pour enregistrer à chaque fois que des paramètres sont supprimés.

Pour l'activer sous drupal 7 :

  1. $conf['sanitize_input_logging'] = TRUE;

Sous drupal 8 :

  1. $settings['sanitize_input_logging'] = TRUE;

Post original du 21/03/2018

Il vient d'être annoncé par l'équipe gérant la sécurité du CMS drupal qu'un gros correctif de sécurité pour drupal 7 et drupal 8 sera déployé mercredi 28/03/2018 entre 20h et 21h30 (heures Françaises). [Edit du 27/03/2018 : j'avais oublié le paramètre « heure d'été » dans mes conversions de timezones...]

On ne sait pas encore où se situe la faille. On sait seulement qu'elle semble (très) importante et qu'il est recommandé d'appliquer le patch immédiatement.

La dernière fois qu'un cas similaire s'est présenté, une fois le correctif disponible, les sites non patchés ont été très rapidement piratés. En effet : une fois que l'on a accès au patch, il est facile de trouver la faille corrigée et ainsi de l'exploiter sur des sites non protégés.

Si vous êtes développeur ou si vous gérez vos propres sites drupal, prévoyez une permanence mercredi soir pour vous occuper de ça.

Si vous avez un site drupal qui est géré par un tiers, assurez-vous qu'il soit au courant et qu'il s'en occupera au plus tôt.

Ce que l'on sait :

  1. Les versions 7 et 8 de drupal sont concernées. Potentiellement aussi la version 6 (pour plus d'informations spécifiques sur la version 6 : https://www.drupal.org/project/d6lts)
  2. Des patchs seront disponible en plus des nouvelles versions de drupal 7, 8.3, 8.4 et 8.5, la mise à disposition d'un patch permet une application du correctif plus rapide et moins risquée qu'une mise à jour complète d'un drupal.
  3. Le correctif ne nécessitera pas de mise à jour de la base de données.
  4. Il faudra aller vite, très vite : lors d'un précédent en 2015 (drupageddon) les premiers sites hackés l'avaient étés moins de 6h après la mise à disposition du patch.
  5. Le patch sera disponible à cette adresse : https://www.drupal.org/psa-2018-001 entre 20H et 21H30 ce mercredi.

 

Plus d'informations : https://www.drupal.org/psa-2018-001

Des petits sites, des gros sites, des micro sites avec Drupal 8

Drupal 8 est un outil dimensionné pour répondre aux besoins des projets web les plus ambitieux. Nous entendons beaucoup parler des notions de headless, de API first, de découplage, etc. qui résolument permettent des architectures solides pour projets ambitieux. Mais ce n'est pas pour autant que Drupal 8 ne propulse plus des sites plus classiques, et voire parfois beaucoup moins ambitieux : de simples, petits, sites Internet, mais pour lesquels nous souhaitons bénéficier de la modularité, de la souplesse et de la robustesse de Drupal.

Par GoZ
Adhérent
Fabien CLEMENT

Installer un Drupal multilingue depuis la configuration

Installer un Drupal multilingue depuis la configuration

Depuis que nous n'avons plus besoin de features pour gérer la configuration et depuis que nous pouvons gérer cette configuration via les fichiers yaml dans un répertoire de configuration, il est beaucoup plus facile de maintenir une configuration entre différents environnements. Depuis quelques temps, il est possible d'installer sans module supplémentaire un Drupal en se basant sur une configuration existante.

GoZ
lun 10/12/2018 - 11:12

Fournir un formulaire personnalisé aux entités de Drupal 8

A l'instar des modes d'affichage qui permettent d'afficher une entité de multiples manières, Drupal 8 permet de créer de multiples modes de saisie, ou formulaires, utilisables sur les entités, que ce soient les utilisateurs, les termes de taxonomy, les contenus ou n'importe quelle entité personnalisée. Découvrons ici comment utiliser ces modes de saisie, depuis leur création jusqu'à leur exploitation pour personnaliser la saisie par exemple des informations d'un utilisateur.

Par Artusamak
Julien Dubois

Créer un système d'annonces simple avec Drupal 8 (seconde partie)

Créer un système d'annonces simple avec Drupal 8 (seconde partie)
DuaelFr
mar 04/12/2018 - 09:30

La première partie de cet article décrivait l'analyse et l'implémentation du système d'annonce présent sur notre site. Dans cette partie nous aborderons l'affichage de l'annonce et la gestion du cache.

Corps

Dans la première partie de cet article, nous avons créé un formulaire qui nous permet de configurer une annonce, sa date de début, sa date de fin et son contenu. Désormais, il nous faut afficher ces informations en respectant la configuration, notamment les dates.

Pour afficher des informations dans une zone définie d'un site Drupal, en dehors de la zone de contenu, nous avons un mécanisme tout désigné : les blocs. Selon la demande initiale, nous devons donc créer un bloc qui sera présent sur toutes les pages du site, lorsque l'annonce est activée et que nous sommes entre la date de début et la date de fin de l'annonce.

Création du bloc

Nous l'avons déjà couvert dans les billets issus de notre formation, la création d'un bloc passe par la création d'un Plugin, soit la forme d'une classe PHP munie d'une Annotation. Dans notre cas, comme pour le formulaire de paramétrage, nous aurons besoin de pouvoir accéder aux données stockées dans la State API et nous aurons donc une dépendance à injecter. La différence principale est que la classe BlockBase n'implémente pas déjà l'interface nécessaire et qu'il faudra donc le faire nous même. Vous le constaterez, du fait que nous manipulions un Plugin et plus juste un formulaire, les méthodes create() et __construct() auront besoin de quelques paramètres complémentaires. Voyons voir ce que l'on doit mettre dans notre fichier src/Plugin/Block/Announcement.php.

namespace Drupal\hc_announce\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\State\StateInterface;

/**
* Provides a 'Announcement' block.
*
* @Block(
*  id = "announcement",
*  admin_label = @Translation("Announcement"),
* )
*/
class Announcement extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * @var array
   */
  protected $config;

  /**
   * Constructs a new Announcement object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param string $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\State\StateInterface $state
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    StateInterface $state
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->config = $state->get('hc_announcement');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('state')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    return ['#markup' => 'Content of the block'];
  }

}

Bien, maintenant que le bloc est créé et après une petite vidange du cache, vous devriez le voir apparaître dans la liste des blocs disponibles dans l'administration du site. Une fois positionné dans la région de notre choix, on a bien la chaîne "Content of the block" qui apparaît sur le site. Tâchons de faire un petit peu mieux en affichant au moins le contenu stocké dans le State pour que les personnes en charge de l'intégration puissent commencer à travailler sur l'aspect visuel. Nous enrichissons donc la méthode build() de notre bloc comme suit.

  /**
   * {@inheritdoc}
   */
  public function build() {
    $build = [];

    $build['#title'] = $this->config['title'];
    $build['#attributes'] = [
      'class' => ['announcement'],
    ];

    $build['announcement'] = [
      '#type' => 'container',
      '#attributes' => ['class' => ['announcement__content']],
      'content' => [
        '#type' => 'processed_text',
        '#text' => $this->config['announcement']['value'],
        '#format' => $this->config['announcement']['format'],
      ],
    ];

    return $build;
  }

Cela fonctionne bien. Cependant, nous ne respectons pas l'état d'activation de l'annonce ni ses dates de début et de fin. Pour l'état de l'annonce rien de plus simple. Avec un test dans la méthode build(), il est toujours possible de renvoyer un tableau vide si l'annonce est inactive. Pour manipuler les dates, nous avons par contre besoin d'une nouvelle dépendance sur le service datetime.time du cœur. Nous ajoutons donc ce service à la méthode create(), à la méthode __construct() et l'enregistrons dans un attribut $time au niveau de l'objet. Puis, nous ajoutons le code suivant à la méthode build() pour renvoyer un bloc vide si le bloc n'est pas supposé être actif.

    if (empty($this->config['enabled'])) {
      return $build;
    }

    $now = $this->time->getRequestTime();
    $start = (new \DateTime($this->config['start_date'] . ' 00:00:00'))->getTimestamp();
    $end = (new \DateTime($this->config['end_date'] . ' 23:59:59'))->getTimestamp();
    if ($now < $start || $now > $end) {
      return $build;
    }

Trop facile ? Vous n'avez pas tort...

Le problème du cache

Comme indiqué en introduction de cet article, ce bloc va être visible sur toutes les pages du site. Il est donc particulièrement important de bien gérer son cache car sinon nous pouvons être confrontés à deux problématiques majeures :

  1. le bloc est affiché quand il est supposé être inactif ou ne s'affiche pas quand il est supposé être actif,
  2. aucune des pages du site n'est mise en cache.

Nous avions déjà abordé le sujet du cache dans Drupal 8 dans un précédent article sans toutefois aborder le cas un peu spécifique des blocs. En effet, ces derniers utilisent les mêmes métadonnées que les render arrays abordés dans l'article mais définies par des méthodes spécifiques : getCacheContexts(), getCacheMaxAge() et getCacheTags(). Ces dernières vont permettre d'indiquer au service BlockManager, comment mettre en cache le bloc dans son intégralité. Lors du rendu d'une page, si le BlockManager se voit demander un bloc qu'il considère comme ayant un cache valide, il va le renvoyer sans même tenter d'instancier sa classe ou d'appeler sa méthode build() ! Par défaut, tous les blocs sont mis en cache de façon permanente et sans aucune variante. Comme pour le reste des métadonnées de cache, ces dernières se propagent vers le conteneur. Il n'est donc pas souhaitable de se faciliter la vie en désactivant totalement le cache de notre bloc sinon cela signifierait désactiver le cache de toutes les pages du site (en vrai c'est mieux fait que ça, mais on simplifie un peu pour avancer).

Gérer la temporalité du cache

Dans notre cas bien particulier, nous allons devoir prévoir trois cas distincts :

  1. si la date de début n'est pas encore arrivée nous devons mettre en cache jusqu'à cette date,
  2. si la date de début est passée mais la date de fin pas encore, nous devons mettre en cache jusqu'à la date de fin,
  3. si la date de début et de fin sont passées ou si l'annonce est désactivée, nous devons mettre en cache de façon permanente.

Pour atteindre ce but, nous allons simplement implémenter la méthode getCacheMaxAge() de la façon suivante :

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    $max_age = parent::getCacheMaxAge();
    if (!empty($this->config['enabled'])) {
      $now = $this->time->getRequestTime();
      $start = (new \DateTime($this->config['start_date'] . ' 00:00:00'))->getTimestamp();
      $end = (new \DateTime($this->config['end_date'] . ' 23:59:59'))->getTimestamp();

      if ($now < $start) {
        $max_age = Cache::mergeMaxAges($max_age, $start - $now);
      }
      elseif ($now < $end) {
        $max_age = Cache::mergeMaxAges($max_age, $end - $now);
      }
    }

    return $max_age;
  }

Notez l'usage de la méthode \Drupal\Core\Cache\Cache::mergeMaxAges() qui permet de proprement fusionner le max-age par défaut avec la valeur que l'on souhaite configurer. Dans notre cas, cela n'aurait rien changé de renvoyer directement la valeur calculée mais c'est une bonne habitude à prendre d'utiliser les méthodes de fusion des métadonnées de cache pour éviter les mauvaises surprises. La méthode mergeMaxAges() s'assure que l'on conserve toujours le max-age le plus contraignant (et c'est exactement pour cette raison que si on désactive le cache d'un bloc, toute la page est impactée).

Gérer les changements de configuration

Maintenance que la temporalité de notre cache est bien établie, il nous faut prendre en compte les changements qui pourraient venir de l'action d'une personne dans l'interface d'administration. En effet, si le titre ou une date change, par exemple, le cache du bloc devrait immédiatement être invalidé pour prendre en compte les nouvelles données. Dans le cas contraire, les gestionnaires du site n'auraient pas d'autre option que d'invalider la totalité du cache du site. Le mécanisme qui permet cette invalidation, ce sont les cache tags. La plupart des objets gérés par le cœur comme les entités de contenu ou de configuration disposent de cache tags pour les identifier mais ce n'est malheureusement pas le cas de la State API. Fort heureusement, c'est un problème très simple à contourner.

Tout d'abord, définissons un cache tag personnalisé et rattachons le à notre bloc grâce à l'implémentation de la méthode getCacheTags(). Son nom est arbitraire alors, comme souvent, nous allons le préfixer du nom du module pour éviter les collisions. Comme précédemment, nous allons utiliser une méthode pour fusionner ce nouveau cache tag avec d'éventuels autres définis par la classe parente.

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    return Cache::mergeTags(parent::getCacheTags(), ['hc_announcement_settings']);
  }

Ensuite, il nous faut juste modifier légèrement le formulaire d'administration de l'annonce pour lui demander d'invalider ce tag lors de l'enregistrement. Pour cela, nous avons besoin de faire appel au service cache_tags.invalidator que nous allons ajouter à notre injection de dépendances et à notre constructeur :

  /**
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
   */
  protected $invalidator;

  /**
   * Constructs a new AnnouncementSettingsForm object.
   *
   * @param \Drupal\Core\State\StateInterface $state
   * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator
   */
  public function __construct(StateInterface $state, CacheTagsInvalidatorInterface $invalidator) {
    $this->state = $state;
    $this->invalidator = $invalidator;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('state'),
      $container->get('cache_tags.invalidator')
    );
  }

Ceci étant fait, il ne nous reste plus qu'à ajouter une petite ligne dans la méthode submitForm() pour provoquer l'invalidation.

$this->invalidator->invalidateTags(['hc_announcement_settings']);

Un dernier "petit" problème

Désormais, notre bloc est mis en cache suivant une logique qui s'appuie sur le mécanisme d'âge maximum pour définir quand l'expiration a lieu de façon très précise. Nous nous attendons donc à ce que le bloc apparaisse ou disparaisse approximativement au moment indiqué et ça fonctionne particulièrement bien... pour les utilisateurs authentifiés...

Le module Internal Page Cache du cœur permet de mettre en cache les pages entières à destination des anonymes afin d'améliorer drastiquement les performances. Le problème, c'est que ce cache, très agressif, ne tient pas actuellement compte des métadonnées de cache et notamment pas du max-age. En attendant que le cœur change de fonctionnement, vous pouvez contourner ce problème en installant le module Cache Control Override, qui ne nécessite aucune configuration particulière.

Conclusion

Au terme de cette paire d'articles, vous devriez y voir plus clair sur la façon de créer un formulaire et un bloc ainsi que sur la gestion du cache. Par cet exemple concret, j'espère que j'aurai réussi à vous faire visualiser des concepts qui étaient traités de façon bien plus académique dans nos précédents articles. S'il reste des zones d'ombre ou si vous voulez en savoir plus sur un point en particulier, n'hésitez pas à vous manifester dans les commentaires ci-dessous.

 

Crédit photo de couverture : Sue Cro

Catégories
Développement
Drupal
Drupal 8
Tags
state api
cache
blocs
Par Christophe MOLLET
Christophe Mollet

Comment installer Drupal 8

Cet article est un tutoriel présentant les étapes à suivre pour installer Drupal 8 sur linux. Il comprend une phase de préparation de l'environnement de travail puis les étapes de l'installation de Drupal 8.

Accessible pour tous et complet, il comprend aussi une section apportant des solutions aux erreurs couramment rencontrées lors de cette installation.

Pages