close

Découvrez nos offres pour faire du digital le levier de votre croissance !

download
Modèles

Téléchargez le Guide Ultime de gestion de projet digitale pour vous aider à piloter vos transformations et faire les bons choix !

Image mise en avant pour l'article

La Form API de Drupal, késako ?

26 octobre 2022
À quoi ressemblerait le Web sans formulaires ? En quoi la Form API est-elle importante ?


Les formulaires, c’est pas la vie, mais presque. Cet outil, devenu indispensable, nous permet de récupérer les données de l'utilisateur et ainsi d'interagir avec lui si besoin. Nous, êtres humains, nous avons les questions, le Web, lui, a ses formulaires.

Cela étant, les formulaires peuvent être la hantise des développeurs. Bien que pas forcément compliqué en soi, sa structure et sa gestion comportent des subtilités et peuvent demander un certain temps de mise en place. Et c'est pourquoi Drupal propose la Form API.

En quoi consiste la Form API sur Drupal ? Découvrons-le ensemble…

 

La Form API, pour quelle utilisation ?

Cela peut paraître trivial, mais, dans quel cas est-il intéressant d'utiliser la Form API ? La réponse : dès que vous avez un formulaire.

Cela peut être : un bouton d'inscription à une newsletter, un formulaire de contact personnalisé, un formulaire multi-étapes, ..., mais également pour gérer des filtres associées à des pages de listes gérés « programmatiquement », pensez-y ! 😜

Aussi, vous est-il déjà arrivé de modifier un formulaire existant via le hook_form_alter ? Par exemple, pour masquer ou rendre non modifiable un champs d'un formulaire du BO. Et bien, vous avez déjà utilisé Form API !

Form API de Drupal, qu'est-ce que c'est ?

 

Mais alors, le Form API, qu'est-ce que c'est ?

C'est une API qui met à votre disposition un ensemble d'outils, de méthodes, d'interfaces, permettant de mettre en place assez facilement un formulaire fonctionnel.

En bref, elle vous permet, via les render array, de générer l'HTML d'un formulaire, d'ajouter simplement quels sont les champs requis, de créer un token de sécurité ou encore de générer un objet spécifique qui contiendra les réponses pour la validation et le traitement des données.

Nous allons donc voir ensemble, comment peut-on se servir de la Form API pour créer un formulaire, gérer sa validation et ses erreurs et comment y insérer de l'AJAX.

 

Comment ça fonctionne ?

Comme je le disais précédemment, la Form API se sert des render array de la Render API pour générer un formulaire. Cela signifie que nous allons devoir fournir à Drupal un tableau possédant une structure particulière, qu'il va ensuite interpréter afin de générer le formulaire souhaité.

 

La base

Pour commencer, je vais créer une classe. Cette classe héritera de la classe FormBase et devra implémenter ces trois méthodes :

  • getFormId()
    Doit renvoyer l'id du formulaire, à définir donc ici.
  • buildForm(array $form, FormStateInterface $form_state)
    C'est ici que la notion de render array est importante, car c'est au sein de cette méthode que nous allons construire ce fameux tableau et le renvoyer pour être affiché.
  • submitForm(array &$form, FormStateInterface $form_state)
    C'est par cette méthode que nous allons définir le comportement du formulaire à sa soumission. À noter que nous rentrons dans cette fonction uniquement si le formulaire a été validé avec succès. Nous reparlerons de ce dernier point dans la section "Validation".

class RegisterForm extends FormBase {

  const FIELD_EMAIL = 'email';
  const FIELD_PASSWORD = 'password';

  public function getFormId(): string {
    return 'register_collaborator';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form[self::FIELD_EMAIL] = [
      '#type' => 'email',
      '#title' => 'Votre mail',
    ];

    $form[self::FIELD_PASSWORD] = [
      '#type' => 'password_confirm',
      '#title' => 'Votre mot de passe'
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => "S'enregistrer",
      '#weight' => 999
    ];

    return $form;
  }

public function submitForm(array &$form, FormStateInterface $form_state) {
    $user = User::create();
    $user->setPassword($form_state->getValue(self::FIELD_PASSWORD));
    $user->enforceIsNew();
    $user->setEmail($form_state->getValue(self::FIELD_EMAIL));
    $user->setUsername($form_state->getValue(self::FIELD_EMAIL));
    $user->save();

    return $user;
  }

}

 

Instanciation du formulaire

Pour afficher (et donc créer) le formulaire, deux solutions s'offrent à vous :

  • Via une route, en créant une route menant directement à ce formulaire ;

example.form:  
  path: '/example-form'  
  defaults:    
    _title: 'Example form'
    _form: '\\Drupal\\example\\Form\\ExampleForm'
  requirements:
    _permission: 'access content'

 

  • Via le service FormBuilder, si vous souhaitez insérer le formulaire où bon vous semble.

$form = \\Drupal::formBuilder()->getForm('Drupal\\example\\Form\\ExampleForm');

 

Récupérer la valeur des champs : FormStates

Un des avantages de la Form Api est qu’à la création (buildForm), à la validation (validateForm) ainsi qu’à la soumission du formulaire (submitForm), nous avons accès à un paramètre de type FormStateInterface.
Ce paramètre est un objet qui possède l’état du formulaire. Il contient donc de nombreuses informations sur celui-ci et notamment toutes les valeurs qui ont été saisies par l’utilisateur.

Ainsi par exemple au sein de la méthode submit, il sera possible de récupérer ces valeurs, les envoyer à son Manager et éventuellement décider de la route vers laquelle l'utilisateur sera redirigé en fonction de ces mêmes valeurs.

 

 

Et en pratique ?

Maintenant, passons à la pratique. Je vous propose de développer 5 étapes : la création, la validation, la soumission, la gestion de l'affichage et je finirai par la gestion des champs en AJAX.

 

Étapes 1 : créer son formulaire

Un formulaire est composé d'un ensemble de champs input, par lesquels les données vont être saisies puis envoyées. La form API propose un certain nombre de FormElements qui sont ensuite convertis en balises HTML.

Ici, j'ai envie de vous présenter les principaux formElements, ainsi que quelques fonctionnalités intéressantes qui pourraient vous faire gagner du temps.

 

> Les principaux types de champs

On retrouve les inputs standards tels que : Text, Checkbox, Number, Date, Email, Radios & radio, Fieldset & Fieldgroup.

Il est possible de configurer ces formElements afin de les personnaliser. Voici les propriétés que j'ai le plus utilisées.

  • description & description_display : définit respectivement la description (texte d'aide) ainsi que la position de cette description par rapport à l'input ;
  • title & title_display : définit respectivement le label de l'input ainsi que sa position par rapport à ce dernier ;
  • placeholder : indique le placeholder de l'élément, lorsque cela s'applique ;
  • required : définit si le champs est obligatoire ou non ;
  • attributes : ici vous pouvez rajouter tous les attributs que vous souhaitez.

Voici par exemple, un input pour une date de naissance :


$form["birthday"] = [
      '#type' => 'date',
      '#title' => "Your birthdate",
      '#weight' => 12,
      '#required' => TRUE,
      '#placeholder' => "Insert here your birthdate",
      '#description' => "The date should match this format: mm/dd/yyyy",
      '#description_display' => 'before',
      '#format' => 'm/d/Y',
      '#attributes' => [
          'autocomplete' => 'off',
          'type' => 'date',
          'max' => (new DateTime('now'))->format('Y-m-d'),
      ],
];

 

> Comment encapsuler ces champs ?

Parfois, la complexité du formulaire peut vous donner envie d'encapsuler les balises dans des <div>, <span>, <p>, etc. Vous vous sentirez alors un peu coincé. Pas de panique, Form API a prévu des outils pour çà : des éléments et des propriétés.

  • Les éléments
    • container : il s'agit de l'élément le plus générique, puisqu'il génère simplement une <div >;
    • html_tag : un peu plus souple que le container puisqu'il permet de générer la balise indiqué dans la propriété tag.
  • Les propriétés
    • field_prefix & field_suffix
    • markup

Voici un exemple d'utilisation d'un container :


$form['optin_group'] = [
      '#type' => 'container',
      '#attributes' => [
          'class' => ['optin-wrapper']
      ],
      '#weight' => 0,
      'optin_general' => [
          '#type' => 'checkbox',
          '#title' => 'I accept general conditions',
          '#default_value' => 0,
      ],
   
];
// On peut également rajouter un élément dedans comme ceci : $form['optin_group']['optin_sms'] = [ '#type' => 'checkbox', '#title' => 'I accept to be contacted by SMS', #default_value' => 0, ];

 

Étapes 2 : valider son formulaire

> Validation côté client

Avant de parler de la validation coté serveur, il est bon de rappeler qu'il est possible de mettre en place, simplement, une validation coté client :

  • Grâce à l'attribut required, qui permet de rendre le champs obligatoire ;
  • Et grâce à l'attribut pattern (introduit depuis HTML5), qui permet de renseigner une regex de contrôle. À la soumission, le navigateur vérifiera si la valeur renseignée dans l'input est validée par la regex. Le cas échéant, le message renseigné au sein de l'attribut title sera affiché.

Par exemple, voici comment construire un champs obligatoire pour un numéro de téléphone :


$form["phone"] = [
      '#type' => 'number',
      '#title' => 'Your phone number',
      '#required' => TRUE,
      '#attributes' => [
        'pattern' => '[0-9]{11}',
        'title' => 'Please insert 10 numbers',
      ]
];

Bien entendu, il est possible de faire davantage de vérification en utilisant JavaScript. Mais bien qu'offrant une excellente expérience utilisateur, ces validations ne sont pas suffisantes en elles-même. En effet, quelqu'un de mal intentionné pourrait facilement contourner ces contrôles en modifiant le DOM à partir du navigateur ou bien en créant une requête HTTP « à la mano ». C'est pourquoi une validation côté serveur est toujours indispensable.

 

> Validation côté serveur

La validation se fait via la méthode validateForm(). Les informations sont récupérées depuis le formstate, puis nous effectuons les vérifications nécessaire aux données récupérées. En cas d'erreur, nous indiquons au formstat la nature de l'erreur ainsi que le champ concerné.


public function validateForm(array &$form, FormStateInterface $form_state) {
    $this->validatePhoneNumber($form, $form_state);
}
public function validatePhoneNumber(array $form, FormStateInterface &$form_state) { $phoneNumberTester = \Drupal::service('my_module.phone_number_tester'); if (!$phoneNumberTester->isValidNumber($form_state->getValue('phone'), 'FR') { $form_state->setErrorByName('phone', t("Your phone number is not formatted correctly")); } }

Une bonne pratique : préférez découper votre fonction de validation en plusieurs sous fonctions, qui auront la charge de valider son propre champs (exemple : validateBirthdate pour valider la date de naissance). Il s'agit du S du concept SOLID (S pour Single responsability !).
Vous ne connaissez pas encore les 5 principes qui composent le concept SOLID 😱, je vous invite à lire notre article intitulé "Comment devenir un Dev SOLID ?" 😉

Ici, j'utilise la méthode $form_state→setErrorByName qui permet d'attribuer une erreur champ phone grâce au nom du champ. Cependant, dans le cas où nous avons des champs imbriqués, le nom du champ en question n'est plus suffisant. En effet, pour récupérer cet élément, Drupal a besoin du nom des champs parents. Nous avons donc deux solutions :

  • Soit vous utilisez la synthaxe parentName][elementName au lieu de elementName . Mais ça peut vite devenir compliqué ;
  • Soit vous utilisez la méthode FormState::setError qui prend comme argument la portion du render array correspondant au champ ( récupérable dans notre exemple $form['phone'] ).

 

Étapes 3 : soumettre son formulaire

Pour permettre la soumission du formulaire, il faut 2 choses :

1. Un ou plusieurs boutons submit

Voici l'exemple d'un bouton submit :


$form['submit'] = [
    '#type' => 'submit',
    '#value' => 'Valider',
    '#weight' => 100
];

 

2. Et renseigner la méthode submitForm, qui sera automatiquement appelée si toutes vos validations ont été passées avec succès. C'est au sein de cette méthode que vous pouvez récupérer les données renseignées dans le form_state, les utiliser et/ou les sauvegarder dans votre structure de données. Vous pouvez également rediriger l'utilisateur en utilisant la méthode $form_state→setRedirectUrl.


public function submitForm(array &$form, FormStateInterface $form_state) {
  $this->manager->setInformation($form_state->getValues());
  $form_state->setRedirect('route_name');
}

 

Toujours dans une optique de découper proprement mon code et de le maintenir dans le temps, vous pouvez remarquer qu'ici je délègue la responsabilité du traitement des données du formstate à une autre classe. Classe que l'on pourrait désigner par Manager.

> Et si nous avons plusieurs boutons, comment fait-on ? Que ce soit de manière synchrone ou asynchrone, il n'y a pas de champs de type button qui puisse imiter le comportement du champ submit. En revanche il est possible d'ajouter autant d'éléments submit que l'on souhaite. 
Ce ne sera plus la méthode submitForm qui sera appelée, mais la méthode qui sera renseigné dans le paramètre #submit du render array correspondant.

 

Étapes 4 : gérer l'affichage

Pour gérer l’affichage de son formulaire, deux solutions se présentent à nous :

  • La première, qui consiste à exploiter au maximum les render array, donc coté back-end ;
  • Et la seconde consistant à déporter une partie de la logique en template twig, donc coté front-end.

 

> Première approche : les render array

La plus « Drupalienne » des solutions serait celle des render array. En effet, ils ont pour rôle de produire du « rendu » à partir des données.
Nous avons vu précédemment qu'il existait plusieurs options pour gérer l’affichage. Ici aussi, il est possible de rajouter tous les attributs que nous souhaitons (id, class, placeholder, etc.), gérer les poids des éléments, leurs titres (label) ainsi que leur description.

En revanche, cette approche présente quelques inconvénients :

  • Les possibilités qu’offrent les render array peuvent être difficiles à appréhender ;
  • Les imbrications d’éléments peuvent être lourdes à mettre en place. Du code très verbeux peut rapidement devenir illisible ;
  • Un intégrateur, avec des connaissances Drupal limitée, pourra difficilement intervenir sur la structuration des éléments HTML. Il sera donc dépendant d’un développeur back-end pour toute modification.

Les deux premiers points sont malheureusement inhérents aux render array et à Drupal, et ne peuvent donc être contournés. En revanche il existe une solution pour déporter une petite partie de la logique au niveau front.

 

> Seconde approche : le theming

Un possibilité qu’offre les render array est d’associer au tableau de rendu : un « theme ». Ce qui correspond à un template. Pour ceci, il faut :

1. Déclarer le thème dans le .module

example.module


function example_theme() {
  return [
    'my_form_theme' => [
      'render element' => 'form',
    ],
  ];
}

 

2. Renseigner le thème dans le render array

Form/MyAwesomeForm.php


class MyAwesomeForm extends FormBase {

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildForm($form, $form_state);

    $form['#theme'] = 'my_awesome_form';
[...]
return $form; }

[...]
}

3. Personnaliser son template

templates/my-awesome-form.html.twig

 

Il est  possible d’insérer des petits morceaux de formulaire au sein de son propre template, en inscrivant , key étant la clé du render array contenant le render élément à afficher.

 

Étapes 5 : gérer ses champs en AJAX

Form API propose la possibilité d’insérer de l’AJAX dans son formulaire. Cela mériterait bien un article à part entière, alors je vais seulement vous présenter les « basics ».

 

> Configurer le render élément

Pour insérer la notion d’AJAX au sein d’un élément, il suffit de lui ajouter la propriété #ajax et d’y insérer quelques propriétés :


'#ajax' => [
    'callback' => '::myAjaxCallback', // don't forget "::" when calling a class method.
    //'callback' => [$this, 'myAjaxCallback'], //alternative notation
    'event' => 'change',
    'wrapper' => 'edit-output', // This element updated with this AJAX callback.
]

  • callback : indique la méthode à exécuter lorsque la requête est reçue par Drupal ;
  • event : indique le type d’événement qui déclenche la requête (click, keyup, change etc.) ;
  • wrapper : indique la portion du code HTLM qui sera modifiée suite à cet appel AJAX. Ce point est utile uniquement si vous renvoyez du code HTML à la prochaine étape.

 

> Spécifier la réponse à l’évènement

Lorsque l’évènement est déclenché, le formulaire envoie une requête AJAX qui appelle la méthode callback renseigné ci-dessus. Il reste maintenant à déterminer ce que va venir modifier ce formulaire « à la volée » et donc ce que devra renvoyer cette méthode callback afin de procéder aux changements.

Là, Drupal vous laisse le choix :

1. Soit vous renvoyez un render element comme dans l'exemple ci-dessous. Ce que je trouve le plus simple et le plus intuitif.


public function myAjaxCallback(array &$form, FormStateInterface $form_state) {
  // Prepare our textfield. check if the example select field has a selected option.
  if ($selectedValue = $form_state->getValue('example_select')) {
      // Get the text of the selected option.
      $selectedText = $form['example_select']['#options'][$selectedValue];
      // Place the text of the selected option in our textfield.
      $form['output']['#value'] = $selectedText;
  }
  // Return the prepared textfield.
  return $form['output'];
}

 

Dans ce cas, nous récupérons l’input à modifier dans le tableau $form, nous modifions cet élément, puis nous le renvoyons. Drupal sait ainsi qu’il doit modifier l’affichage de cet élément (sans rafraîchir la page, la magie de l’AJAX !)

2. Soit vous renvoyez une portion de HTML (html markup).

public function myAjaxCallback(array &$form, FormStateInterface $form_state) {
  $markup = 'nothing selected';

  // Prepare our textfield. Check if the example select field has a selected option.
  if ($selectedValue = $form_state->getValue('example_select')) {
      $selectedText = $form['example_select']['#options'][$selectedValue];
      $markup = "$selectedText";
  }

  // Don't forget to wrap your markup in a div with the #edit-output id
  // or the callback won't be able to find this target when it's called
  // more than once.
  $output = "<div id='edit-output'>$markup</div>";

  // Return the HTML markup we built above in a render array.
  return ['#markup' => $output];
}

 

Avez-vous remarqué que dans l'exemple ci-dessus, la portion de HTML a été encapsulée dans un wrapper (défini plus haut) comme tel :

'wrapper' => 'edit-output'

 

3. Enfin, vous choisissez d’envoyer des AjaxCommand. Ce sont des objets qui vont se transformer en instruction au niveau de client.

 

En conclusion, la Form API peut être difficile à appréhender et peut parfois même s'apparenter à une usine à gaz, d'autant que cet article n'a pas la prétention d'être exhaustif sur ce sujet !

Mais lorsque l'on a compris son fonctionnement, son utilisation peut vraiment vous faire gagner du temps ! Personnellement, je vous conseille de commencer par l'utiliser pour des formulaires simples, de jouer avec les formElements et une fois prêt, lancez-vous dans l'AJAX !

Cet article vous a été utile ? Je vous conseille de lire également les articles suivants :

 

Crédit photo : gorodenkoff

Image mise en avant pour l'article
Quentin Rinaldi
Développeur Web
L'urgence de migrer sur Drupal 9 : conseils et bonnes pratiques
Voir le webinar !
Vous maitrisez la technologie Drupal et vous souhaitez participer à une aventure humaine ?
Adimeo recrute un Développeur Drupal Senior capable d’encadrer des projets et d’accompagner des développeurs juniors...
Voir notre offre d'emploi !
Pourquoi s'abonner à
notre newsletter ?
Pour recevoir tous les mois, un condensé de contenus utiles et pertinents pour votre transformation digitale !