Gifty #3 - Création des entités

Dans cet article

Cet article est le troisiè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.

Les développeurs backend vont être content, nous rentrons enfin dans du fonctionnel !

00:00:00

Aujourd'hui, je suis ravi parce qu'enfin, on quitte les paillettes et la poudre aux yeux pour du fonctionnel ! Ne vous trompez pas, je ne critique pas du tout le dev front qui est extrêmement important et utile. Mais développeur logiciel à la base, si je fais aujourd'hui du web et de la prestation Drupal fullstack, je dois bien avouer avoir une réelle appétence pour le développement backend, et plus généralement pour l'algorithmie complexe. Alors pas d'algorithme lourd ici évidemment, ça reste simple ! Mais nous allons parler services, entités, champs et autres et ça j'aime !

Comme toujours, je démarre mon chronomètre en ligne et accompagne mon développement de musique. Cette fois-ci ce sera la playlist Epic Gaming sur Spotify.

Génération du squelette des entités

Pour notre projet, nous devons gérer des listes-cadeaux. Ces listes se déclinent en différents types : la liste de Noël, liste de naissance, liste de mariage, etc… La différence entre ces types ? Honnêtement, je n'en sais rien à ce stade. Peut-être que par la suite, nous aurons des champs spéciaux par type, du style "nom du conjoint" pour la liste de mariage, "nom de l'enfant" et "sexe du bébé" pour la liste de naissance. En bref, c'est pour être aussi générique que possible et être plus évolutif et tolérant à d'autres idées.

Lorsque vous développez pour un projet dont le périmètre fonctionnel est incertain ou amené à évoluer, considérez toujours une option la plus générique possible - dans le budget et temps imparti bien entendu.

A mon avis, un développement trop spécifique et restrictif est certes (parfois) plus performant, plus simple à concevoir ou plus facile à mettre en place. Mais l'expérience en situation réelle me laisse penser qu'à trop fermer sa solution, on se tire souvent une balle dans le pied. Il est parfois beaucoup moins coûteux de créer quelque chose d'un peu plus générique dès le début, que de devoir refaire par la suite. Imaginez une entité sans déclinaisons de type (bundle) que vous devrez rendre "bundable" (rendre déclinable par type) par la suite, alors que le site est en ligne avec déjà du contenu : ce n'est pas si simple.

D'un autre côté, il ne faut pas non plus sur-ingénieriser nos solutions. Pas toujours simple de trouver le bon équilibre !

Dans mon cas, il y a trop de risques que j'ai des idées ultérieures concernant les types de liste-cadeaux pour que je ne prenne pas quelques secondes de plus pour adjoindre des bundles à mes listes-cadeaux.

A whole structure was generated with Drupal Console
Toute une structure générée par Drupal Console

Vous êtes perdu avec ce que j'évoque ? Pensez contenu et type de contenu. De la même manière que pour les contenus de Drupal, nous voulons ici des types de listes cadeaux auxquels nous pourrons configurer des champs. C'est ce que, d'un point de vue technique, nous exprimerons entre connaisseurs Drupal comme "besoin d'une entité liste-cadeaux bundable et fieldable".

Ne cherchez pas à faire rentrer dans le contenu node quelque chose qui n'est pas une page web de contenu.
Un client grand compte a fait appel à moi en urgence pour un site Drupal 8 sur lequel de l'achat en ligne (sans Drupal Commerce) avait été mis en place. Les "tickets de caisse", aussi bien clients qu'internes avaient été faits en les sauvegardant dans l'entité node en créant pour l'occasion un type de contenu "facture d'achat". Ces pseudo factures étaient sauvegardées avec le statut "dépublié" afin d'être rendues privées. Vous sentez l'arnaque arriver ? Tout est fait dans Drupal pour que les nodes soient le mieux optimisés possibles pour le référencement. Et les choses n'ont pas manqué, suite à une erreur de manipulation d'un contributeur, l'ensemble des nœuds tout type confondu ont été publiés. Metatags a fait son boulot, le sitemap aussi, et en un rien de temps des milliers de transactions se sont retrouvées sur google, parfaitement référencées, et exposant bien entendu des données privées. Sachez que la CNIL est intransigeante et impose des amendes pour ce genre de cas !

Une entité répond par définition à une mission fonctionnelle propre et je déconseille de faire rentrer les carrés dans les ronds. Créer une entité personnalisée est vraiment simple et rapide, surtout grâce à Drupal Console, alors pourquoi s'en priver ?

Pour notre part, nous allons en créer deux : giftlist sera notre liste-cadeau, et gift représentera un cadeau au sein de la liste. Le temps de trouver ces noms et de réfléchir à mon découpage a été plus long que la création pratique de ces deux entités. Vraiment ! La preuve :

Nous commençons par créer un module quasi vide via la commande lancée depuis le dosser racine de Drupal:

../vendor/bin/drupal generate:module

Notre module existe désormais et se nomme Gifty (gifty). Nous allons y créer notre première entité :

../vendor/bin/drupal generate:entity:content

Yes, yes, no, no, au travers de quelques questions, nous créons tout ce qu'il faut pour avoir notre fameuse entité giftlist avec les bundles giftlist_type. Non seulement cela, mais Console nous a aussi généré tout l'écosystème autour : les formulaires de création, édition, suppression, les droits d'accès, le contrôleur d'accès, la génération des URLs correspondantes : la liste est longue !

On recommence une fois pour la seconde. Cette fois-ci, l'entité gift sera simple, sans bundle.

En littéralement trois commandes et cinq minutes, nous venons de créer au sein de notre projet un module personnalisé avec une architecture déjà assez balèze !

Personnalisations et corrections

Honnêtement, Drupal Console est parfois bluffant. Pourtant, cela reste du code génériquement généré et tout n'est donc pas parfait. En l'occurrence quelques petites choses me dérangent.

  1. Les URLs de mes entités ne correspondent pas à mes attentes.
    Drupal Console a généré toutes mes entités avec des URLs du type /admin/structure/giftlist/{giftlist}. Or, j'aimerais des URLs plus simples, et surtout qui fassent moins "admin" puisque mes utilisateurs pourront gérer leurs propres listes eux-mêmes. Je modifie donc la partie annotation de mes deux entités, dans la section links où sont définit les patterns d'URL par type d'opération.
    1
    2
    3
    4
    5
    6
    7
    8
    
    *  links = {
    *    "canonical" = "/giftlist/{giftlist}",
    *    "add-page" = "/giftlist/add",
    *    "add-form" = "/giftlist/add/{giftlist_type}",
    *    "edit-form" = "/giftlist/{giftlist}/edit",
    *    "delete-form" = "/giftlist/{giftlist}/delete",
    *    "collection" = "/admin/gifty/giftlist",
    *  },
  2. Les opérations sur mes entités doivent utiliser mon thème front.
    Par défaut, Console considère que les opérations et les formulaires de création, édition, suppression d'entités sont réservées aux admins, et donc présentées via le thème admin. Ce choix du thème se fait par l'usage d'une option spécifique dans la déclaration de la route. Pour une entité, la définition des routes est le boulot d'une classe de type RouteProviderInterface. Par défaut, il s'agit de DefaultHtmlRouteProvider ou de celle déclarée dans la section route_provider de l'annotation de notre classe. Console a fait le boulot de générer une classe pour nous, mais qui étend AdminHtmlRouteProvider. Revenons à l'héritage de la classe de base, et surchargeons uniquement les routes collection et add-page pour lesquelles vous avez constaté que je souhaite, pour le coup, l'usage du thème admin. Je modifie également la route créée par Console pour lister les bundles disponibles pour cette entité. Voici l'exemple pour l'une des routes concernées :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    /**
     * {@inheritdoc}
     */
    protected function getCollectionRoute(EntityTypeInterface $entity_type) {
      if ($route = parent::getCollectionRoute($entity_type)) {
        $route->setOption('_admin_route', TRUE);
        return $route;
      }
    }
  3. Je crée une nouvelle entrée dans la barre de menu pour la gestion de mon projet.
    J'avoue que c'est un peu superflu, mais quelle classe ! J'ai créé un post dédié à ce sujet pour expliquer comment je m'y prends, mais me voilà à créer des routes et des liens de menus à tout va dans mon module, ce qui est vraiment plus simple que ça en a l'air !
    Menus de l'administration de Gifty dans la barre d'outils
    Menus de l'administration de Gifty dans la barre d'outils
  4. J'augmente la profondeur de la barre d'outils.
    Malheureusement, la barre d'outils est hardcodée à une profondeur de 4 et comme l'on peut le voir sur l'image au-dessus, je veux aller loin dans la structure des raccourcis. Là aussi, j'ai écrit un post dédié à cette technique.
  5. J'ajoute les liens dynamiques pour la création d'une liste-cadeau d'un type donné.
    Bon, là, j'avoue c'est pour me la jouer ! Totalement inutile et donc indispensable, j'ajoute les liens "Ajouter une liste de …" comme sur l'image ci-dessous pour les types de listes de cadeaux. Ces liens sont générés dynamiquement, forcément, via l'usage un deriver approprié. 
    Liens de menus générés dynamiquement
    Liens de menus générés dynamiquement
  6. Je déplace le code par-ci par-là pour une structure plus simple et cohérente.
    Console, pour ne pas se marcher dans les pattes, génère chaque entité de manière totalement séparée et autonome. Or certaines choses peuvent être refactorisées ou déplacées au même endroit. Par exemple, il est inutile d'avoir un fichier entity.pages.inc pour chaque preprocess. Typiquement, les deux peuvent aller dans le même fichier. 

Mine de rien, le temps passe vite et une grosse demi-heure vient ainsi de passer ! 

Définition des champs de base

Notre entité giftlist est déclinable en multiple bundles pouvant présenter des champs de natures différentes. Certains de ces champs en revanche sont communs à ce qui définit l'essence même de notre entité. C'est-à-dire que toute liste-cadeau, peu importe son type, aura forcément au moins ces champs-là.

On appelle alors ces champs des base fields. Ils ne sont pas créés par l'administrateur du site via l'interface de Drupal comme les champs habituels auxquels vous pourriez être habitués. Au contraire, ils sont créés directement dans le code source par le développeur de l'entité, de sorte que l'application peu compter de façon certaine sur leur présence pour son fonctionnement. D'un point de vue technique, contrairement aux autres champs disposant de leurs propres tables, les champs de base seront directement des colonnes de la table principale de l'entité (définie par l'annotation base_table ou data_table selon la configuration de votre entité).

Les performances sont meilleures sur les champs de base que sur les champs contribués qui possèdent chacun leur propre table et nécessitent donc des jointures pour leur récupération.

Dans la création de vos entités custom, le plus long sera souvent de réfléchir à la définition de ces base fields, de se tromper, de changer d'avis, et finalement de revenir à l'idée initiale. Il ne faut en effet ni trop en imposer au risque de se retrouver trop fortement contraint dans la suite, ni en négliger au risque de manquer de donnée dans le scope fonctionnel.

J'envisage donc un certain nombre de champs. Il y a des champs assez basiques, comme created et changed pour les timestamps de création et modification de l'entité. Des types plutôt logiques, comme le champ gifts associant les cadeaux à la liste-cadeau. Et certains plus surprenants comme visibility qui définit le mode de visibilité de la liste qui peut être publique, privée ou protégée par mot de passe. De la même manière, mode définira le mode de fonctionnement de la liste-cadeau qui peut fonctionner en mode surprise, semi-surprise ou sans surprise selon ce que le propriétaire de la liste pourra savoir des cadeaux choisis par ses amis.

Puisque l'on parle de propriétaire de l'entité : what the hell ?! Drupal Console réinvente la roue en recodant le champ owner et ses accesseurs plutôt que d'utiliser le trait EntityOwnerTrait !

Voici quelques changements que je réalise au sein de mon entité :

  • Je définis des labels surchargés type bundle_label ou encore label_plural là où c'est nécessaire.
  • Je supprime la définition du champ owner pour utiliser le trait EntityOwnerTrait.
  • Je supprime l'implémentation de l'interface EntityPublishedInterface et supprime donc le champ status et la clef published associée. En effet, pour les entités liste-cadeau et cadeau, la notion de publié ou non-publié n'a pas de sens.
  • Je modifie temporairement les contrôles d'accès de l'entité pour ne plus faire de check sur la notion disparue de publication.
  • Enfin, j'ajoute mes champs de base pour mes deux entités.

Voici par exemple, la définition de quelques champs de l'entité giftlist :

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
// Determines access conditions to the giftlist in view mode.
/* @see \Drupal\gifty\GiftListAccessControlHandler */
$fields['visibility'] = BaseFieldDefinition::create('list_string')
  ->setLabel(t('Visibility'))
  ->setDescription(t('The visibility condition for this Giftlist entity.'))
  ->setDefaultValue(GiftList::VISIBILITY_PUBLIC)
  ->setSetting('allowed_values', GiftList::getVisibilityConditions())
  ->setDisplayOptions('form', [
    'type' => 'options_select',
    'weight' => -2,
  ])
  ->setDisplayConfigurable('form', TRUE)
  ->setDisplayConfigurable('view', TRUE)
  ->setRequired(TRUE);
 
// Determines how notifications and information displayed will be handled.
$fields['mode'] = BaseFieldDefinition::create('list_string')
  ->setLabel(t('Notification mode'))
  ->setDescription(t('The notification mode associated with this Giftlist entity.'))
  ->setDefaultValue(GiftList::NOTIFICATION_ALL)
  ->setSetting('allowed_values', GiftList::getNotificationModes())
  ->setDisplayOptions('form', [
    'type' => 'options_select',
    'weight' => -2,
  ])
  ->setDisplayConfigurable('form', TRUE)
  ->setDisplayConfigurable('view', TRUE)
  ->setRequired(TRUE);
 
// Intersection with the gift entity (1...n).
// NOTE: gift fieldtype cardinality is always 1.
/* @see \Drupal\gifty\Plugin\Field\FieldType\GiftItem */
$fields['gifts'] = BaseFieldDefinition::create('gift')
  ->setLabel(t('Gifts'))
  ->setDescription(t('Gift handling for this entity.'))
  ->setDisplayOptions('form', [
    'region' => 'hidden',
  ])
  ->setDisplayOptions('view', [
    'label' => 'hidden',
    'weight' => -2,
  ])
  ->setDisplayConfigurable('form', TRUE)
  ->setDisplayConfigurable('view', TRUE);

Bilan

Voilà une bonne heure et demie que je code quand enfin, je suis content de mon résultat et des deux entités nouvellement créées. D'une part, nous avons un très joli menu dans la barre d'outils, mais celui-ci permet l'administration de deux entités assez importantes. La liste-cadeau (giftlist) est une entité associée à des bundles. Elle référencie des cadeaux (gift). C'est pas mal !

Pour autant, créer des entités personnalisées est quelque chose de très courant dans un projet client un peu intéressant. Pas autant que l'usage ou la création de plugin, mais tout de même, j'ai l'habitude. Alors pourquoi tant de temps ? Le tâtonnement mon ami ! Avec un tantinet plus de spécification fonctionnelle, j'aurais gagné énormément de temps sur la mise en place des champs. J'ai fait, puis défait, puis refait plusieurs fois !

Comment être certain de ne rien oublier ou au contraire de ne pas trop en mettre ?
Une liste-cadeau doit-elle par exemple avoir une date de fin ?
Cette date de fin est-elle un champ de base ou est-ce que je l'ajouterais plus tard ?
Est-ce qu'un cadeau doit avoir un "propriétaire" comme la liste ou le fait d'appartenir forcément à une liste suffit ?
Qui connaît quoi ? La liste connaît ses cadeaux ou les cadeaux connaissent-ils leur liste ?
Pourquoi ne pas ordonner les cadeaux par préférence ? Oui mais ce n'est pas du base field ça, ce n'est pas essentiel à la notion de cadeau
Les cadeaux ont-ils forcément un prix ?

Bref, vous voyez le genre !

Vue de la page de création d'une liste-cadeau
Le fonctionnel est là, mais y'a du boulot pour la présentation !

Au fait, n'avez-vous pas remarqué un truc dans le code à la fin du dernier paragraphe ?

Nous avons défini dans le code un champ nommé gifts et de type gift. Mais qu'est-ce qu'un champ de type gift ? Pour Drupal pour l'instant ça n'existe pas !

Impossible donc de réellement tester à ce stade puisque le champ de type gift n'existe en réalité pas encore. C'est l'objet du prochain article sur ce défi.

Mais à présent dodo ! Il est 2h30 du matin quand je finis d'écrire ces lignes et bientôt ma fille va se réveiller la couche chargée. Daddy couche a intérêt à dormir un peu !

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 :)