Gifty #4bis - Refactoring

Dans cet article

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.

Astuce de pro : pour plus de concentration, faites un tour aux toilettes ! Ça détend, et c'est là que viennent les meilleures idées. Sous la douche, ça marche aussi, mais attention à la consommation d'eau chaude !

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.

La nouvelle solution:
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.

Pourquoi alors Drupal ne fait pareil avec la gestion des commentaires ?
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 !

Ajout d'un pseudo-champ
Le champ de base que nous supprimons (1) et le nouveau pseudo-champ créé (2). En tant que pseudo-champ, il ne permet aucune configuration d'aucune sorte comme peut l'avoir un champ.

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.

Le code peut sembler complexe à un débutant parce qu'il est long, mais il est quasi-trivial:
  • 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
Configuration de la vue giftlist_gifts
Configuration de la vue giftlist_gifts.
Via la UI à la page /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 !

  • Vue d'une page de liste-cadeau
  • Vue d'une page de liste-cadeau
Le fonctionnel est parfaitement au point, mais Dieu que c'est moche !

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

Votre nom sera affiché publiquement avec votre commentaire.
Votre email restera privé et n'est utilisé que pour vous notifier de l'approbation de ce commentaire.
Sur internet, vous pouvez être qui vous voulez. Soyez quelqu'un de bien :)