Dans la série sur la Création d'un module avec Drupal 8, nous avons pu avancer sur une entité. Maintenant que notre type d'entité est généré et son implémentation bien avancée, que nous avons également un service qui peut nous procurer des données concernant nos entités, nous allons pouvoir personnaliser son formulaire d'édition.
Je dois avouer que la partie concernant le formulaire d'édition m'a pris beaucoup de temps. Non pas que le principe soit compliqué, mais je voulais un résultat bien précis et il a fallu comprendre le fonctionnement de certaines classes du noyau avant de pouvoir finaliser le formulaire.
L'héritage et la surdéfinition
C'est la classe src/Form/InternalLinkForm.php qui nous intéresse aujourd'hui. La classe InternalLinkForm hérite de ContentEntityForm, ce qui semble logique, puisque notre entité est de type "contenu" (donc chaque entité est enregistrée dans la base de données, dans une table propre). InternalLinkForm est la classe qui détermine le comportement du formulaire d'édition du type d'entité internal_link. Elle contient à la base 2 méthodes: buildForm() et save().
La méthode buildForm construit le formulaire. Par défaut, elle fait appel à sa méthode parent
$form = parent::buildForm($form, $form_state);
pour récupérer le formulaire construit puis le retourne. Si on se réfère à Drupal 7, buildForm est un peu comme un callback de type form (drupal_get_form). Cependant, l'intérêt ici réside dans l'héritage de classe. La classe parent, ContentEntityForm, s'occupe de construire un formulaire en ajoutant la base nécessaire. Dans certains cas, une classe telle que InternalLinkForm n'aurait même pas besoin d'exister, si nous n'avions pas à modifier le comportement du formulaire.
La méthode save() quant à elle, intercepte le clic sur le bouton de sauvegarde de l'entité et permet d'influencer l'entité, soit avant sa sauvegarde, soit après. Vous aurez certainement remarqué que nous trouvons également un appel à la fonction parent:
$status = parent::save($form, $form_state);
C'est lors de cet appel que l'entité est enregistrée dans la base de données. Nous pouvons donc modifier l'entité avant sa sauvegarde en plaçant du code avant cet appel, ou effectuer des actions post-sauvegarde en plaçant du code après cet appel.
La hiérarchie d'héritage de notre classe est la suivante:
InternalLinkForm -> ContentEntityForm -> EntityForm -> FormBase.
Si vous remontez cette hiérarchie, vous pourrez voir toute une série de méthodes disponibles. La plupart de ces méthodes, en tout cas toutes celles de type public et protected, pourraient être surdéfinies pour modifier leur comportement. Dans la classe FormBase par exemple, nous trouvons une méthode
public function validateForm(array &$form, FormStateInterface $form_state) {...}
Qui correspond à la méthode de validation du formulaire. Ici, elle est vide. Mais en allant dans la classe ContentEntityForm, nous retrouvons cette méthode, implémentée cette fois-ci, pour valider les éléments de base du formulaire.
Il est tout à fait possible de redéclarer cette méthode dans notre classe InternalLinkForm pour modifier son comportement, exemple:
public function validateForm(array &$form, FormStateInterface $form_state) { // Do stuff before parent validation $entity = parent::validateForm($form, $form_state); // Do stuff after parent validation return $entity; }
L'important résidant surtout dans le fait de ne pas oublier d'appeler la méthode du parent lorsqu'elle est implémentée, afin de garder le processus standard tout en y ajoutant ses propres fonctionnalités.
Je parle ici de sudréfinition de formulaire, mais avec la POO (programmation orientée objet) le principe est le même partout. Dès lors qu'une classe hérite d'une autre, il est possible de surdéfinir les méthodes publiques (public) et protégées (protected) pour y ajouter sa propre logique.
Fonctionnalités de base et esthétique
Nous allons maintenant voir comment ajouter nos propres fonctionnalités dans ce formulaire.
Deux points sont à aborder ici. Tout d'abord, la partie "Type d'url" (fonctionnalité), puis les onglets verticaux (esthétique).
Type d'url
Souvenez-vous, nous avons ajouté plusieurs champs dans notre type d'entité via la méthode baseFieldDefinitions de notre classe InternalLink. Parmi ces champs, nous trouvons 4 champs qui doivent fonctionner d'une certaine manière:
- Type d'url (url_type)
- Url standard (url)
- Type d'entité (url_entity_type)
- Entité (entity_id)
Le type d'url est une liste déroulante qui indique si notre url est de type standard (url simple au format http://...) ou si elle correspond à une entité particulière dans le système. Si le type choisi est standard, nous afficherons le champ "Url standard" et masquerons les champs "Type d'entité" et "Entité". Dans le cas contraire, c'est le champ "Url standard" qui sera masqué et les deux autres affichés.
Pour ajouter cette fonctionnalité, nous allons travailler avec la propriété #states des éléments de formulaire. Cette propriété bien pratique avait été introduite dans Drupal 7 et a été maintenue dans Drupal 8. Elle permet d'indiquer un état à appliquer (visible, invisible, required, etc...) selon une ou plusieurs conditions.
Dans notre classe, nous allons rajouter la méthode suivante:
public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); if (isset($form['url']['widget'])) { $form['url']['#states'] = [ 'visible' => [ ':input[name="url_type"]' => ['value' => 'standard'], ], ]; $form['url']['widget'][0]['value']['#states'] = [ 'required' => [ ':input[name="url_type"]' => ['value' => 'standard'], ], ]; } if (isset($form['url_entity_type']['widget'])) { $form['url_entity_type']['#states'] = [ 'visible' => [ ':input[name="url_type"]' => ['value' => 'entity'], ], ]; $form['url_entity_type']['widget']['#states'] = [ 'required' => [ ':input[name="url_type"]' => ['value' => 'entity'], ], ]; } // Prepare url field with wrapper (@see url_entity_type field below) // and add states to hide url when no entity type is selected. if (isset($form['entity_id']['widget'])) { $form['entity_id']['widget'][0]['target_id']['#states'] = [ 'visible' => [ ':input[name="url_type"]' => ['value' => 'entity'], ], 'invisible' => [ [ [':input[name="url_type"]' => ['value' => 'standard'],], [':input[name="url_entity_type"]' => ['value' => '_none'],], ], ], 'required' => [ ':input[name="url_type"]' => ['value' => 'entity'], ] ]; } return $form; }
Nous définissons les états à respecter selon des critères précis, par exemple:
$form['url']['#states'] = [ 'visible' => [ ':input[name="url_type"]' => ['value' => 'standard'], ], ];
indique que le champ "Url standard" ne doit être visible que dans le case ou le champ "Type d'url" possède la valeur "standard".
Onglets verticaux (vertical tabs)
Dans le cadre de mes recherches concernant la création de module, j'avais investigué la surdéfinition du formulaire d'édition du module "node" et j'avais pu constater que la partie "Alias d'url" était placée dans les onglets verticaux plutôt que dans le formulaire de base. Suite à cette découverte, j'ai voulu faire de même pour certains champs de mon entité, à savoir les champs "author", "created", "class", "rel" ainsi que les champs de visibilité "visibility" et "except_list".
La logique est assez simple. Il suffit de déclarer des éléments de formulaires de type #vertical_tabs et #details, ces derniers devant posséder une propriété #group permettant d'indiquer l'appartenance au parent.
Voici donc à quoi ressemble la fonction form après ces modifications:
public function form(array $form, FormStateInterface $form_state) { // Prepare a vertical tabs to store several fields $form['advanced'] = [ '#type' => 'vertical_tabs', '#attributes' => ['class' => ['entity-meta']], '#weight' => 99, ]; $form = parent::form($form, $form_state); // Internal link author information for administrators. $form['author'] = [ '#type' => 'details', '#title' => t('Authoring information'), '#group' => 'advanced', '#attributes' => [ 'class' => ['internal-link-form-author'], ], '#weight' => 90, '#optional' => TRUE, ]; if (isset($form['uid'])) { $form['uid']['#group'] = 'author'; } if (isset($form['created'])) { $form['created']['#group'] = 'author'; } // Internal link information for administrators. $form['internal_link'] = [ '#type' => 'details', '#title' => t('Internal link information'), '#group' => 'advanced', '#attributes' => [ 'class' => ['internal-link-form-details'], ], '#weight' => 80, '#optional' => TRUE, ]; if (isset($form['class'])) { $form['class']['#group'] = 'internal_link'; } if (isset($form['rel'])) { $form['rel']['#group'] = 'internal_link'; } if (isset($form['visibility'])) { $form['visibility']['#group'] = 'internal_link'; } if (isset($form['except_list'])) { $form['except_list']['#group'] = 'internal_link'; } if (isset($form['url']['widget'])) { $form['url']['#states'] = [ 'visible' => [ ':input[name="url_type"]' => ['value' => 'standard'], ], ]; $form['url']['widget'][0]['value']['#states'] = [ 'required' => [ ':input[name="url_type"]' => ['value' => 'standard'], ], ]; } if (isset($form['url_entity_type']['widget'])) { $form['url_entity_type']['#states'] = [ 'visible' => [ ':input[name="url_type"]' => ['value' => 'entity'], ], ]; $form['url_entity_type']['widget']['#states'] = [ 'required' => [ ':input[name="url_type"]' => ['value' => 'entity'], ], ]; } // Prepare url field with wrapper (@see url_entity_type field below) // and add states to hide url when no entity type is selected. if (isset($form['entity_id']['widget'])) { $form['entity_id']['widget'][0]['target_id']['#states'] = [ 'visible' => [ ':input[name="url_type"]' => ['value' => 'entity'], ], 'invisible' => [ [ [':input[name="url_type"]' => ['value' => 'standard'],], [':input[name="url_entity_type"]' => ['value' => '_none'],], ], ], 'required' => [ ':input[name="url_type"]' => ['value' => 'entity'], ] ]; } return $form; }
L'élément "advanced" déclaré avant la récupération du formulaire par la méthode parent construit le système d'onglets verticaux. Puis les éléments "author" et "internal_link" définissent deux onglets. Leur propriété #group indique qu'ils doivent appartenir à l'élément "advanced". Enfin, les véritables éléments de formulaire utilisent également la propriété #group pour indiquer dans quel groupe ils doivent être placés.
Conclusion
Notre formulaire commence à être personnalisé. Il reste encore plusieurs points à aborder mais c'est un début.
Vous avez pu vous rendre compte qu'il est aisé de surdéfinir une méthode, appeler la méthode parent pour garder le comportement standard et rajouter son propre code pour intégrer ses propres fonctionnalités.
Nous verrons par la suite comment surdéfinir le comportement de certains champs pour obtenir un formulaire correspondant réellement à nos besoins.