Cet article est le quatrième de la suite "10h pour créer un business en ligne". Je recommande évidemment d'avoir lu les articles précédents pour continuer. Pour rappel, ce défi consiste en la création d'une version alpha (ou POC - Proof Of Concept) d'un outil de gestion de liste-cadeaux en ligne, du type liste de mariage, de naissance ou de Noël et autres.
00:00:00
Chronomètre en ligne… musique… Bref, vous connaissez désormais ma routine, alors passons ! Comme un mauvais Marvel, je vous propose ici un reboot de la partie 4. git revert ! lancé comme une formule magique, nous effaçons la partie 4 et réécrivons tout dans cet article, mais en 20 minutes au lieu d'une heure trente s'il vous plaît !
Alors que s'est-il passé ? Et bah… Je suis allé aux toilettes !
Bon plus sérieusement, une nuit a passé et comme d'habitude, ma fille s'est réveillée et nous avons entamé le classique cycle de couches, tétage, berçage, pleurnichage, marchage, endormissage (ordre et formulation non contractuelle). Il est donc quatre heures du matin quand, assis sur les toilettes et la tête dans le coltard quand je réfléchis à la situation.
Concrètement, je réfléchissais à la suite de cette série d'article et je me disais deux choses :
- Je veux tout ajaxifier sur cette page. Mais comment ajaxifier la pagination de ma liste ?
- Est-ce que je pourrais mettre en place un chargement automatique au défilement (un infinite scroll comme les posts sur facebook)
- Comment mettre efficacement en cache ma liste de cadeau et son rendu ?
Et à ces questions, la réponse est : en l'état, il faut du code. Pourtant, Drupal core possède déjà un outil capable de gérer des listes. Un outil dont c'est même l'unique but dans l'existence ! Le module Views bien entendu.
Il y a autre chose qui me dérange fortement. Après avoir écrit l'article 4, un goût amer me reste :
- J'ai créé un type de champ qui finalement ne fait quasi rien
- J'ai tellement besoin de rien que j'ai dû coder volontairement un widget vide pour éliminer le widget côté édition
- Mon champ n'a tellement rien à stocker en tant que champ que j'ai dû lui affecter arbitrairement une valeur fictive toujours vraie
La solution a en fait été déjà évoquée au sein de l'article : le pseudo-champ.
Un pseudo champ disponible côté display et effectuant le double rendu d'une vue affichant les cadeaux référencés par une liste cadeau donnée en filtre contextuel, et toujours notre formulaire de création
Alors pourquoi n'ai-je pas pensé à tout ça avant ? Voilà la grande question que vous allez vous poser sur bien des projets. Et c'est le jeu ma pauvre Lucette ! Parfois, c'est le manque de connaissance technique, parfois le nez dans le guidon et le manque de recul, parfois la pression de la deadline, parfois on n'y pense juste pas.
Dans le cas présent, c'est probablement la fatigue de ma situation de jeune papa couplé au fait que ce soit un projet sans enjeu. Aveuglé par la ressemblance de notre besoin avec la solution de Drupal pour les commentaires, j'ai foncé dans le mur comme un taureau sur un chiffon rouge. Reprenant le code et élaguant selon mes besoins, je ne me suis aperçu que trop tard que la solution en devenait inutile et chronophage pour si peu.
Si encore il n'y avait pas Views, on aurait pu se dire comme dans l'article précédent que l'idée était plus pérenne en cas de futur besoin en configuration. Mais si Views fait le boulot de création de liste, alors toute configuration passera par cet outil et le champ devient inutile. De plus, en comparaison, la création d'un pseudo-champ c'est une affaire de quelques minutes.
Nous sommes en train de créer un module custom dans un projet utilisant Drupal. Le module Commentaire de core est lui une petite partie d'un système modulaire fait pour vous permettre de n'activer que ce dont vous avez besoin. Non seulement créer une dépendance du module Comments sur Views serait une aberration, mais en plus le champ commentaire possède de la configuration spécifique, un besoin d'édition côté widget et un besoin d'affichage en front. Bref, bien que fondamentalement similaire, la gestion des commentaires reste plus complexe et surtout plus générique dans Drupal core, tout en maintenant le besoin d'indépendance pour l'activation ou la désactivation de la fonctionnalité.
0 à 20 minutes
Incroyablement simple ! En 20 minutes, nous allons tout mettre en place et même mieux que nous l'avions fait en une heure trente précédemment.
Un peu de ménage
Première chose : j'enlève tout ce qui a été introduit en partie quatre, et l'on revient donc à la fin de la partie 3.
- Je vire la définition du champ gifts des basefields de notre entité cadeau dans la classe de l'entité
Gift.php
- Je vire aussi celle du champ field_name qui avait dû être introduite pour supporter le fait qu'il soit techniquement possible de mettre plusieurs champs gifts par entité giftlist.
- Je vire ensuite toutes les classes de
src/Plugin/Field
: la définition du champ, le widget et le formatter. - Je vire enfin le
hook_preprocess_field
pour préprocesser le champ gift
Création et rendu du pseudo-champ
En second lieu, je crée le pseudo-champ, grâce au hook_entity_extra_field_info
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * Implements hook_entity_extra_field_info(). */ function gifty_entity_extra_field_info() { foreach (\Drupal\gifty\Entity\GiftListType::loadMultiple() as $bundle) { $extra['giftlist'][$bundle->id()]['display']['gifties'] = [ 'label' => t('Gifties'), 'description' => t('Gifts associated with this gift list.'), 'weight' => 50, 'visible' => TRUE, ]; } return $extra; }
Grâce cette simple fonction, un nouveau pseudo-champ est disponible dans la configuration du style d'affichage de chacun de mes types de liste cadeau. J'ai appelé ce champ gifties histoire de trouver un nom !
Maintenant que ce champ existe, il va falloir lui donner du contenu. Pour faire cela, je décide de surcharger la classe responsable du rendu d'une entité : EntityViewBuilder
. Ma nouvelle classe s'appellera GiftListViewBuilder
et pour que l'entité GiftList l'utilise pour son rendu, je dois modifier l'annotation view_builder de la définition de l'entité pour :
1 2 3 * handlers = { * "view_builder" = "Drupal\gifty\GiftListViewBuilder", ...
L'une des méthodes de cette classe, buildComponents permet le rendu des champs de l'entité. C'est cette méthode que je vais surcharger pour ajouter le code de rendu de mon nouveau champ. Et là, toute l'astuce tient en quelques lignes : j'effectue le rendu d'une vue nommée giftlist_gifts (qu'il nous faudra créer) et le rendu de notre formulaire.
- j'injecte les dépendances de la classe d'origine, plus celle que j'ai besoin en extra
- je surcharge la méthode buildComponents responsable du rendu des champs de l'entité
- je rajoute le rendu de mon champ s'il est présent dans l'affichage. Il comprend :
- le rendu d'une vue
- le rendu du formulaire d'ajout
Et comme je suis un dingue dans ma tête : je lazybuild le rendu du formulaire !
Cliquez pour ouvrir/fermer ce bloc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 <?php namespace Drupal\gifty; use Drupal\Core\Config\Entity\ConfigEntityStorageInterface; use Drupal\Core\Config\StorageInterface; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityViewBuilder; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Theme\Registry; use Drupal\gifty\Entity\Gift; use Drupal\views\Views; use Symfony\Component\DependencyInjection\ContainerInterface; /** * View builder handler for giftlists. */ class GiftListViewBuilder extends EntityViewBuilder implements TrustedCallbackInterface { /** * The views storage. * * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface */ protected $viewStorage; /** * {@inheritdoc} */ public function __construct(EntityTypeInterface $entity_type, EntityRepositoryInterface $entity_repository, LanguageManagerInterface $language_manager, Registry $theme_registry, EntityDisplayRepositoryInterface $entity_display_repository, ConfigEntityStorageInterface $view_storage) { parent::__construct($entity_type, $entity_repository, $language_manager, $theme_registry, $entity_display_repository); $this->viewStorage = $view_storage; } /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $entity_type, $container->get('entity.repository'), $container->get('language_manager'), $container->get('theme.registry'), $container->get('entity_display.repository'), $container->get('entity_type.manager')->getStorage('view') ); } /** * {@inheritdoc} */ public function buildComponents(array &$build, array $entities, array $displays, $view_mode) { parent::buildComponents($build, $entities, $displays, $view_mode); /** @var \Drupal\gifty\Entity\GiftListInterface $entity */ foreach ($entities as $id => $entity) { $bundle = $entity->bundle(); $display = $displays[$bundle]; // If gifties has been configured to be displayed. if ($display->getComponent('gifties')) { // Build the view display of the gifts. $view = $this->viewStorage->load('giftlist_gifts'); // Note: injected equivalent here of Views::getView('giftlist_gifts'). $viewExecutable = $view->getExecutable(); $viewExecutable->setArguments([ 'giftlist_id' => $entity->id(), ]); $viewExecutable->setDisplay('default'); $viewExecutable->preExecute(); $viewExecutable->execute(); // Build the fied render array. $build[$id]['gifties'] = [ '#theme' => 'gifties', 'gifts' => $viewExecutable->buildRenderable(), 'gift_form' => [ '#lazy_builder' => [ get_called_class() . '::renderAddForm', [ $entity->id(), ], ], '#create_placeholder' => TRUE, ], ]; } } } /** * #lazy_builder callback; builds a gift add form. * * @param string $giftlist_id * The giftlist ID. * * @return array * A renderable array representing the gift add form. */ public static function renderAddForm($giftlist_id) { // Build the add form. $gift = Gift::create([ 'giftlist_id' => $giftlist_id ]); // Get the form render array. return \Drupal::service('entity.form_builder')->getForm($gift); } /** * {@inheritdoc} */ public static function trustedCallbacks() { $callbacks = parent::trustedCallbacks(); $callbacks[] = 'renderAddForm'; return $callbacks; } }
Notez que dans le code source de la page, je peux vérifier mon callback et le lazyload de mon formulaire d'ajout de cadeau en trouvant en lieu et place du formulaire un data-big-pipe-placeholder-id.
Création de la vue
La dernière étape consiste en la création de la vue que nous chargeons au sein du nœud. Celle-ci se fait directement dans la UI d'admin :
- son id doit être giftlist_gifts
- il s'agit d'une vue sur l'entité Gift
- l'affichage se fait en entité rendue via le display par défaut
- l'affichage se fait au format grille par lot de 3
- j'ajoute un pager tous les 50 éléments
- j'ajoute un filtre contextuel sur le champ giftlist_id pour filtrer les cadeaux appartenant à la liste de cadeau donnée
/admin/config/development/configuration/single/export
soit l'export de configuration seule, ne pas oublier d'exporter la vue créée pour l'ajouter au dossier config/install
de notre module afin que celle-ci soit ajoutée automatiquement à l'installation du module.Voilà, c'est tout ! En 20 minutes à peine, montre en main, nous venons de recréer la fonctionnalité de l'article #4, tout en étant plus générique dans les possibilités fonctionnelles. Notre vue pourra utiliser AJAX, nous pourrons ajouter des critères de tri, des filtres, et en fait, ajouter tout ce que les vues peuvent faire !
Bilan
En littéralement 20 minutes, je viens de refaire ce qui m'avait pris une heure et demi, avec une technique plus simple et une évolutivité plus grande.
J'aurais pu ne pas écrire l'article 4 et passer directement à celui-ci, mais je pense que c'est intéressant de montrer ce processus de réflexion. Il arrive parfois de ne penser à une solution plus simple que plus tard. Il arrive aussi que ce ne soit qu'une fois le fonctionnel clairement posé que l'on peut entrevoir un refactoring possible.
Dans un projet réel, il arrive souvent que l'on ne nous donne pas le temps de ce refactoring. C'est une des causes de la dette technique selon moi. Imaginez que le client souhaite maintenant de l'AJAX partout sur cette page, ou un chargement des cadeaux type défilement infini. Sur cette méthode-ci, ce sera simple comme bonjour, via les modules de communauté pour Views. Sur la méthode de l'article précédent, bon courage ! Il faudra produire une tonne de code, avec tous les bugs potentiels qui vont avec. Et à une couche pas terrible, on se retrouve à ajouter une autre couche pas terrible, et une rustique, puis un patch, puis un peu de scotch, et enfin on finit avec un truc tout de guingois qui certes fonctionne, mais que plus personne n'ose toucher de peur que cela s'écroule comme un château de carte. Ici, nous avons utilisé l'outil permettant de faire des listings pour construire notre liste : ça semble bien mieux ainsi !
Je pense sincèrement que le refactoring fait partie de la vie d'un projet, et qu'il est normal de s'en laisser parfois le temps, à mesure que le fonctionnel s'étoffe et que l'équipe de développement acquiert du savoir-faire ainsi qu'une vue d'ensemble sur le projet. Le choix technique d'hier, n'est pas toujours la super-solution de demain.
Waouh, trop de philosophie ! Il est temps de retourner à l'art simple de bercer ma fille :)
Ajouter un commentaire