Les concepts de programmation dans Drupal
4 avril 2021
Durant mes années d’expérience, j’ai pu constater une certaine défiance envers Drupal de la part de développeurs utilisant d’autres technologies. Ce logiciel a une image de CMS « click click » ou de style de programmation PHP procédurale. Avec Drupal 8, de nombreux paradigmes comme la programmation orientée objet (POO) et les design patterns ont été introduits par des librairies externes. Ces derniers permettent à Drupal d’utiliser les mêmes standards que l’ensemble des frameworks modernes.
Module
Lorsqu’on l’installe Drupal, un ensemble de modules l’accompagne automatiquement. Ces modules regroupent un ensemble de logiques de code pouvant apporter une nouvelle fonctionnalité ou en étendre une déjà existante. Ils ont évolué, passant d’une programmation de type procédurale dans Drupal 7 à une approche objet afin de se rapprocher des standards actuels. C’est similaire au concept de bundle dans Symfony. Prenons le temps d’aborder quelques concepts disponibles au sein d’un module en analysant le module « block » du cœur de Drupal.
block
|—— block.api.php
|—— block.info.yml
|—— block.install
|—— block.libraries.yml
|—— block.links.contextual.yml
|—— block.links.menu.yml
|—— block.links.task.yml
|—— block.module
|—— block.permissions.yml
|—— block.post_update.php
|—— block.routing.yml
|—— block.services.yml
|—— config/
|—— css/
|—— js/
|—— migrations/
|—— src
| |—— BlockAccessControlHandler.php
| |—— BlockForm.php
| |—— BlockInterface.php
| |—— BlockListBuilder.php
| |—— BlockPluginCollection.php
| |—— BlockRepositoryInterface.php
| |—— BlockRepository.php
| |—— BlockViewBuilder.php
| |—— Controller/
| |—— Entity/
| |—— EventSubscriber/
| |—— Form/
| |—— Plugin/
| |—— Tests/
| |—— Theme/
|—— templates/
ı—— tests/
On peut voir des contrôleurs « block/src/Controller/”, un système derouting “block/block.routing.yml”, la définition de services “block/block.services.yml”, des entités “block/src/Entity/”, des assets “block/css/“ , “block/js/” et même des dépendances à des librairies externes.
Une des notions fondamentales de Drupal est le hook. De type procédural, il va permettre aux modules d’interagir. Lorsqu’un module va mettre à disposition un hook, les autres modules vont pouvoir l’implémenter pour y ajouter de nouvelles fonctionnalités ou modifier les existantes. Concrètement, lorsqu’un hook est déclenché, il va parcourir l’ensemble des modules et thèmes qui l’ont surchargé. Pour ce faire, il faut respecter une certaine nomenclature ‘my_module_hook_name ()’. « my_module” correspond au nom du module qui l’a implémenté. « hook_name” correspond au nom du hook à surcharger.
Un des plus connus est ‘hook_preprocess ()’qui permet de pré-traiter les variables avant qu’elles ne soient rendues. On le retrouve la plupart du temps dans le fichier *.theme, et ‘hook_xxx_alter()’qui permet d’altérer des informations avant qu’elles ne soient rendues.
Avec l’arrivée de la programmation orientée objet et des composants de Symfony, deux nouveaux concepts sont voués à remplacer les hooks dans le futur :
- Les Plugins : c’est une idée intégrée dans Drupal pour étendre la configuration de fonctionnalités graphiques. De façon objet, un module « parent » va mettre à disposition une classe abstraite pour définir un modèle à suivre. Les autres modules pourront alors créer une classe qui étend ce “plugin” en lui définissant un comportement spécifique. Comme exemples de plugins, nous avons les “FieldFormatter”,“Block“, “EntityType ».
- Les Events : ce concept reprend le composant « EventDIspatcher” introduit par Symfony. Cela va permettre à des composants de communiquer entre eux en envoyant des événements et en les écoutant. Dans Drupal, on va définir une classe qui implémente “EventSusbcriber Interface”. Elle nous force à redéfinir la fonction “get SubscribedEvents”. Cette fonction retourne un tableau de noms d’événements que l’abonné souhaite écouter. Pour chaque nom, on lui associe une ou des fonctions qui seront appelées lorsque celui-ci sera exécuté.
class BlockPageDisplayVariantSubscriber implements EventSubscriberInterface {
/**
*Selects the block page display variant.
*
*@param \Drupal\Core\Render\PageDisplayVariantSelectionEvent $event
* The event to process.
*/
public function onSelectPageDisplayVariant(PageDisplayVariantSelectionEvent $event) {
$event->setPluginId('block_page');
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[RenderEvents::SELECT_PAGE_DISPLAY_VARIANT][] = ['onSelectPage
DisplayVariant'];
return $events;
}
}
Derniers concepts que j’aimerais aborder dans cette partie : l’apparition des Services et de l’injection de dépendance. Ce concept est repris de la POO. Drupal met à disposition des centaines de fonctionnalités découplées dans le but qu’elles soient le moins dépendantes possible les unes des autres. Le service container va s’occuper d’instancier les objets dont on a besoin. Voici un exemple de déclaration de service avec Drupal. On a le fichier mon_module.services.yml à la racine de notre module.
On peut le déclarer comme ceci :
services:
mon_module.mon_service:
class:[mon_namespace]\ma_classe
arguments: []
Ensuite, on va utiliser le service container afin d’instancier nos services par injection de dépendance dans nos contrôleurs.
// MonSuperController.php class MonSuperController extends ControllerBase { protected $entityQuery ; protected $maClasse ; public function __construct(QueryFactory $entity_query, MaClasse $ma_classe) { $this->entityQuery = $entity_query ; $this->maClasse = $ma_classe ; } public static function create(ContainerInterface $container) { return new static( $container-> get('entity.query'), $container-> get('mon_module.mon_service') ) ; } }
Voici les principaux concepts présents dans Drupal lors de la création de modules dont je voulais vous parler. Si vous avez l’occasion de créer un module, il est important d’anticiper la conception d’une fonctionnalité générique et une instanciation spécifique pour répondre précisément au besoin. Ainsi, la partie générique pourra être partagée avec d’autres projets…
Entity API
Dans son histoire, Drupal a évolué par son expérience. Dans Drupal 6, tout était nœud, puis dans Drupal 7, tout est devenu entité. Drupal 8 a ajouté la couche objet aux entités. Deux types d’entités sont présents :
- des entités de configuration que nous aborderons un peu plus loin dans cet article ;
- des entités de contenu.
Concernant ces dernières, il est important de connaître celles qui sont disponibles par défaut dans Drupal pour modéliser correctement ses données et son contenu. Les plus connues sont les nœuds (les pages par exemple), les utilisateurs, les taxonomies. On peut également créer nos propres types d’entités. Le but de ces entités est de stocker en base de données les contenus générés par les utilisateurs. Vos entités, en fonction de leur configuration, mettent à disposition une personnalisation des champs, gèrent le multilingue et les révisions.
Si on s’intéresse de plus près à la définition d’une entité, comme le type d’entité nœud, on s’aperçoit qu’elle est définie dans le dossier » node/src/entity/Node.php ». La classe implémente l’annotation “ContentEntityType” qui étend le plugin de type « EntityType ».
*@ContentEntityType( * id = "node", * label = @Translation("Content"), * label_collection = @Translation (« Content"), * label_singular = @Translation (« content item"), * label_plural = @Translation (« content items"), * label_count = @PluralTranslation( * singular = "@count content item", * plural = « @count content items" *], * bundle_label = @Translation("Content type"), * handlers = { * "storage" = "Drupal\node\NodeStorage", * "storage_schema" = "Drupal\node\NodeStorageSchema",
* "view_builder" = "Drupal\node\NodeViewBuilder", * "access" = "Drupal\node\NodeAccessControlHandler", * "views_data" = "Drupal\node\NodeViewsData", * "form" = { * "default" = "Drupal\node\NodeForm", * "delete" = "Drupal\node\Form\NodeDeleteForm", * "edit" = "Drupal\node\NodeForm", * "delete-multiple-confirm" = "Drupal\node\Form\DeleteMultiple" * }, * "route_provider" = { * "html" = "Drupal\node\Entity\NodeRouteProvider", * }, * "list_builder" = "Drupal\node\NodeListBuilder", * "translation" = "Drupal\node\NodeTranslationHandler" * }, * base_table = « node", * data_table = "node_field_data", * revision_table = "node_revision", * revision_data_table = "node_field_revision", * show_revision_ui = TRUE, * translatable = TRUE, * list_cache_contexts = { "user.node_grants:view" }, * entity_keys = { * "id" = "nid", * "revision" = "vid", * "bundle" = "type", * "label" = "title", * "langcode" = "langcode", * "uuid" = "uuid", * "status" = "status", * "published" = "status", * "uid" = "uid", * "owner" = "uid", * }, * revision_metadata_keys = { * "revision_user" = "revision_uid", * "revision_created" = "revision_timestamp", * "revision_log_message" = "revision_log" * }, * bundle_entity_type = « node_type", * field_ui_base_route = "entity.node_type.edit_form", * common_reference_target = TRUE, * permission_granularity = "bundle", * links = { * "canonical" = "/node/{node}", * "delete-form" = "/node/{node}/delete", * "delete-multiple-form" = "/admin/content/node/delete", * "edit-form" = "/node/{node}/edit", * "version-history" = "/node/{node}/revisions", * "revision" = "/node/{node}/revisions/{node_revision}/view", * "create" = "/node", * } *)
Dans l’annotation, on détermine l’ensemble des données qui vont définir notre entité. C’est également ici que l’on va définir si notre entité est traduisible et mettre en place le système de gestion de versions, appelées révisions.
Gestion de la configuration
Que ce soit pour avoir de la configuration réutilisable, ou pour déployer nos modifications de configuration sur nos différents environnements, Drupal 7 a engendré une importante frustration chez une partie de ses développeurs. Avec Drupal 8, le projet « Content Management Initiative » (CMI) – mettant à disposition une nouvelle API – est né dans le but d’avoir de la configuration exportable et importable facilement. Cela permet de séparer la configuration spécifique du site dans des fichiers YAML. Remarque importante, par défaut CMI exporte l’ensemble de la configuration. Il n’est pas possible, par défaut, d’isoler une partie de celle-ci.
Deux autres notions majeures sont à comprendre avec CMI :
- Active directory / Staging Storage : après l’installation de notre site, on va pouvoir exporter la configuration qui lui est propre. L’ensemble des fichiers est exporté dans un répertoire du système de fichier. C’est ce qu’on appelle le « Staging storage”. L’avantage ici, c’est qu’avec un système de version comme GIT nous avons un suivi de l’évolution des configurations. Lorsque l’on modifie un fichier de la configuration de notre site et qu’on l’importe pour la mettre à jour, cela va mettre à jour notre « Active Storage« . C’est l’endroit où est stockée la configuration courante de notre site.
- Configuration de données simple / Entité de configuration : deux types de configurations, la « Donnée simple” et l’“Entité de configuration”. Le type de configuration « donnée simple » entrepose des données de type basique. On l’utilise pour des cas spécifiques comme des configurations propres à notre module. Pour les manipuler, je vous laisse regarder du côté du service “config.factory“. Quant au type de configuration « Entité de configuration », il entrepose la configuration plus complexe. Cela va permettre à des types d’entités de sauvegarder leurs données de configuration dans des fichiers YML. Par exemple, voici quelques types d’entités de configurations : les vues, les rôles, les menus, les styles d’images. En analysant les exemples précédents, on s’aperçoit que ce sont des types d’entités qui peuvent avoir de multiples instances qui leur sont propres.
Je vous conseille de jeter un œil aux modules « config_ignore » et « config_split ». Ils vous permettront d’ignorer certains fichiers de configuration que vous ne voulez pas écraser et avoir une gestion plus fine de configurations par environnement.
Multilingue / internationalisation
Drupal 8 a repris le meilleur de son passé et s’est transformé pour arriver avec 4 modules par défaut qui structurent l’ensemble du multilingue en son cœur. Il va gérer les langues, mais aussi tous les concepts d’internationalisation qui lui sont liés (les dates, le sens des textes de gauche ou de droite).
Voici une présentation des 4 modules majeurs du cœur :
- Langage module : ce module vous permet de choisir la langue de base à l’installation de votre site. À tout moment après l’installation de votre site, vous pouvez ajouter une nouvelle langue dans l’interface d’administration.
- Locale (Interface Translation) module : ce module vous permet, grâce à son interface, de traduire l’ensemble des textes affichés en front. Pour vous aider avec les traductions du cœur qui en compte plus de dix mille actuellement dans cette version 9. La communauté complète le site localize.drupal.org auquel il se connecte automatiquement pour mettre à jour les traductions.
- Content Translation module : ce module est celui qui va vous permettre de traduire le contenu. Avec son interface d’administration on va pouvoir choisir de traduire une entité, plus finement un de ces « bundle« , ou encore plus précisément le champ d’un “bundle”.
- Configuration Translation : ce module vous permet de traduire vos valeurs de configuration. Lorsque vous exportez vos fichiers de configuration, vous aurez alors un dossier de langue qui contient les données de votre configuration dans une langue spécifique.
D’après mon expérience, deux concepts de programmation sont importants à connaître.
Tout ce qui s’affiche en front est traduisible.
Drupal contient un Trait « StringTranslationTrait » qui va vous permette de traduire ce que vous souhaitez dans une classe. La fonction que vous utiliserez le plus souvent est celle-ci :
$this->t('J’adore les %food.', [ '%food » => $my_food, [‘context » => ‘Mon site’] ])
Elle comprend trois paramètres : une chaîne de caractères, les variables dynamiques liées, et, enfin, ce que nous rajoutons par défaut à chaque fois, un contexte. En effet, il est possible d’avoir deux fois la même chaîne de caractères que vous souhaitez différente en fonction du contexte. Il existe également une fonction qui vous permet de gérer les singuliers et pluriels.
Une fonction de traduction fournie par Drupal est disponible en Javascript et dans Twig. Il faut charger nos entités dans la bonne langue. Maintenant que nous avons traduit l’ensemble de notre front, qu’en est-il de nos contenus traduits dans notre back-office ? Si vous souhaitez charger un contenu en fonction du langage courant ou du navigateur ou autre, Drupal met à disposition le LanguageManager service.
Ce service va vous permettre entre autres choses de charger votre contenu d’entité en fonction de la langue passée en paramètre.
if ($item->hasTranslation ($langcode)) { $item = $ item->getTranslation($langcode); }
L’exemple ci-dessus permet de vérifier que votre contenu existe dans la langue voulue avant de vouloir le changer. Dernier conseil avant de clôturer cette partie, suite à nos retours d’expérience, et ce même si votre site n’est pas multilingue, implémentez ces méthodes.
Sécurité
Drupal 8 est populaire et reste un des CMS privilégiés du marché. Il est préconisé pour les sites à fort trafic. Cette popularité attire des personnes à la recherche de la moindre faille à exploiter. Heureusement, depuis sa création, Dries Buytaert, son concepteur, a pris très au sérieux la partie sécurité. Drupal a sa propre équipe de sécurité recherchant les vulnérabilités qui peuvent être présentes dans le cœur mais aussi dans les modules de la communauté. Des mises à jour de sécurité sont alors disponibles dans de brefs délais. Il ne vous reste plus qu’à mettre à jour vos modules.
J’ai trouvé des données statistiques des failles de sécurité majeures que Drupal a recensées ces dernières années. Voici une petite présentation des types de failles qui reviennent le plus souvent et des outils mis en place pour y remédier.
Cross Site Scripting (XSS)
Drupal 8 a fait un grand pas en avant dans ce domaine avec l’utilisation du moteur de template Twig. Afin de sécuriser le système contre les attaques de type XSS, il vérifie automatiquement toutes les variables alimentées par Twig. Cela permet d’avoir une protection contre les attaques XSS en utilisant correctement les API de rendu fournies. Drupal dans son API a mis à disposition un certain nombre de fonctions, afin de garantir la sûreté de vos chaînes. Voici la liste :
- t() pour les chaînes traduisibles ;
- Html::escape() pour les textes simples ;
- Xss:filter() pour les chaînes avec du HTML dedans ;
- UrlHelper::stripDangerousProtocols() pour checker les URL.
ByPass something
Cette faille permet à un attaquant de provoquer une atteinte à la confidentialité des données. Pensez à mettre en place des règles de droit d’accès rigoureuses sur vos pages. Drupal met à disposition un système avancé de permission que vous pouvez faire évoluer.
Exécution de code
Cela consiste à exploiter une faille dans le CMS qui pourrait permettre de l’exécution de code à distance. Souvent lié aux formulaires, pensez à toujours bien vérifier toutes les données envoyées depuis vos formulaires.
CSRF
C’est une faille qui permet aux attaquants de transmettre des commandes non autorisées d’un site tiers au site qui fait confiance à l’utilisateur donné. Ses résultats peuvent être similaires à ceux de XSS, mais son fonctionnement est légèrement différent. Drupal 8 protège ses ressources REST contre les attaques CSRF en exigeant l’envoi d’un en-tête de requête X-CSRF-Token lorsqu’il utilise une méthode non sécurisée. Ainsi, lors de requêtes non en lecture seule, ce jeton est requis. La recommandation de Drupal est de ne jamais utiliser de formulaires HTML personnalisés et d’utiliser plutôt l’API de formulaire.
Tests
Dans sa version 8, Drupal a adopté l’utilisation du framework PHPUnit, toujours dans le but d’utiliser les standards de la communauté PHP. Voici un exemple de structure d’implémentation des tests dans un module Drupal.
text —— config/ |—— migrations/ |—— src/ |—— tests | ı —— src | |—————— Functional | | |——————— TextFieldTest.php | | |——————— TextRequiredSummaryUpdateTest.php | |—————— FunctionalJavascript | | |——————— TextareaWithSummaryTest.php | |—————— Kernel | | |——————— Migrate | | |——————— TextFormatterTest.php | | |——————— TextSummaryTest.php | | ı——————— TextWithSummaryItemTest.php | ı—————— Unit | |———————— Migrate/ | ı———————— Plugin/ |——text.es6.js |——text.info.yml |——text.js |——text.libraries.yml |——text.module ı——text.post_update.php
Comme on peut le voir dans la capture ci-contre, un répertoire « tests » se situe à la racine de notre module. Voici le chemin des fichiers :
/modules/[mon_module]/tests/src/[TypeDeTest]
Il existe un répertoire type pour chacun de nos quatre types de tests :
- Unit : cela correspond à nos tests unitaires afin de tester des méthodes de classe individuellement. Ce sont les tests les plus rapides à exécuter ;
- Kernel : test avec le chargement du noyau, accès à la base de données et à une liste limitée de modules activés ;
- Functional : test avec une instance complète de Drupal, possibilité d’ajouter des modules complémentaires ;
- FunctionalJavascript : comme les tests fonctionnels mais avec un navigateur émulé afin de tester le JavaScript.
Lorsque vous créez un test, celui-ci doit respecter cette structure de l’espace de nom :
Drupal\Tests\[mon_module]\[TypeDeTest]
Les classes de tests doivent toujours finir par le terme “MaClassTest » et pour finir, les fonctions commencent par le mot “testDeMaFonction”.
On arrive au terme de la présentation des concepts que je souhaitais vous présenter. J’espère vous avoir davantage donné envie de découvrir ce CMS. Drupal depuis sa version 8 n’a pas peur de se remettre en question et se modernise continuellement, N’hésitez pas à venir faire un tour sur le slack de Drupal France : drupalfrance.slack.com si vous avez des questions, et, pourquoi pas, un jour, participer aux issues de la communauté pour proposer vos évolutions.
Pour aller plus loin
Cet article est tiré du magazine Programmez!
Voir l'article sourceDéveloppeur Web