Aller au contenu principal
Aller au contenu principal

Bonjour, je suis Karim Boudjema. Développeur back-end senior établi à Montréal, au Canada, passionné par Drupal, l'IA et les tests automatisés.

Créer un formulaire personnalisé en Drupal 11

Créer des formulaires fait partie du quotidien du développeur Drupal. Les formulaires sont des tableaux de rendu imbriqués dans toutes les versions de Drupal, mais là où Drupal 7 les définissait dans des fonctions, Drupal 8 et les versions suivantes les définissent dans une classe de formulaire.

Dans ce post, nous allons construire un formulaire personnalisé avec deux champs, un champ texte et une case à cocher : nous les validerons, afficherons les valeurs dans un message et redirigerons l'utilisateur vers la page d'accueil. L'exemple original en Drupal 8 se trouve sur github.com/KarimBoudjema/Drupal8-ex-custom-form ; le code ci-dessous est réécrit pour Drupal 11.

web/modules/custom/ex81/
|-- ex81.info.yml
|-- ex81.routing.yml
`-- src
    `-- Form
        `-- HelloForm.php

En Drupal 11, tout formulaire est une classe qui implémente \Drupal\Core\Form\FormInterface, laquelle définit quatre méthodes :

  • getFormId() : l'identifiant unique du formulaire ;
  • buildForm() : s'exécute quand le formulaire est demandé ; renvoie le tableau de rendu $form ;
  • validateForm() : s'exécute à l'envoi ; vérifie les valeurs et déclenche éventuellement des erreurs ;
  • submitForm() : s'exécute sur un envoi valide pour traiter les valeurs.

Nous allons construire toute la classe étape par étape, mais vous pouvez d'abord générer le squelette avec Drush 13 :

drush generate module
drush generate form:simple

1. La classe du formulaire et son identifiant

Créez src/Form/HelloForm.php avec une classe qui étend la classe abstraite FormBase (qui implémente FormInterface). Commencez par getFormId(), qui renvoie un nom machine unique :

<?php

declare(strict_types=1);

namespace Drupal\ex81\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * A simple two-field custom form.
 */
final class HelloForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    return 'ex81_hello_form';
  }

}

2. Construire le formulaire

Vient ensuite buildForm(), qui définit une description, le champ texte, la case à cocher et un bouton d'envoi, puis renvoie le tableau de rendu :

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state): array {
    $form['description'] = [
      '#type' => 'item',
      '#markup' => $this->t('Please enter the title and accept the terms of use of the site.'),
    ];

    $form['title'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Title'),
      '#description' => $this->t('Enter the title of the book. It must be at least 10 characters long.'),
      '#required' => TRUE,
    ];

    $form['accept'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('I accept the terms of use of the site'),
      '#description' => $this->t('Please read and accept the terms of use.'),
    ];

    // Wrap submit handlers in an "actions" element so they are styled
    // consistently and other modules can add buttons.
    $form['actions'] = [
      '#type' => 'actions',
    ];
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

3. Valider le formulaire

À présent, validateForm(). Nous lisons les valeurs envoyées avec $form_state->getValue('key') et déclenchons des erreurs avec $form_state->setErrorByName(). Ici, nous rejetons un titre de moins de 10 caractères, ainsi qu'une case non cochée :

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    $title = (string) $form_state->getValue('title');
    if (mb_strlen($title) < 10) {
      $form_state->setErrorByName('title', $this->t('The title must be at least 10 characters long.'));
    }

    if (empty($form_state->getValue('accept'))) {
      $form_state->setErrorByName('accept', $this->t('You must accept the terms of use to continue.'));
    }
  }

4. Traiter les données

Enfin, submitForm(). C'est ici que vous pourriez enregistrer en base de données, appeler une API externe ou passer les données à un service ; pour notre part, nous nous contentons d'afficher les valeurs et de rediriger vers la page d'accueil avec $form_state->setRedirect().

Un changement important en Drupal 11 : drupal_set_message() a été supprimée il y a longtemps. Utilisez le service messenger à la place. FormBase l'expose déjà via $this->messenger() :

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $this->messenger()->addMessage($this->t('Title: @title', [
      '@title' => $form_state->getValue('title'),
    ]));
    $this->messenger()->addMessage($this->t('Accepted: @accept', [
      '@accept' => $form_state->getValue('accept') ? $this->t('yes') : $this->t('no'),
    ]));

    $form_state->setRedirect('<front>');
  }

Si vous vouliez enregistrer ces valeurs dans la configuration, vous étendriez plutôt ConfigFormBase, comme nous l'avons vu dans le billet sur le formulaire de configuration.

5. Créer une route vers le formulaire

Ajoutez une route dans ex81.routing.yml. Comme il s'agit d'un formulaire, utilisez la clé _form (et non _controller) pour que Drupal invoque le constructeur de formulaire :

ex81.hello_form:
  path: '/ex81/helloform'
  defaults:
    _form: 'Drupal\ex81\Form\HelloForm'
    _title: 'Simple custom form example'
  requirements:
    _permission: 'access content'

Rendez-vous sur /ex81/helloform et testez-le. Rien de bien compliqué, non ?

6. Bonus : injecter proprement le service messenger

Appeler $this->messenger() est pratique, mais pour tout ce qui dépasse le trivial vous devriez injecter vos dépendances explicitement. Comme FormBase implémente ContainerInjectionInterface, le trait de cœur AutowireTrait fonctionne ici exactement comme dans un contrôleur. Déclarez la dépendance comme une propriété de constructeur promue et typée, et faites l'économie de la méthode create() écrite à la main :

<?php

declare(strict_types=1);

namespace Drupal\ex82\Form;

use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;

/**
 * The same form, with the messenger service injected via autowiring.
 */
final class HelloForm extends FormBase {

  use AutowireTrait;

  public function __construct(
    protected readonly MessengerInterface $messengerService,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    return 'ex82_hello_form';
  }

  // getFormId(), buildForm() and validateForm() are identical to ex81.

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $this->messengerService->addMessage($this->t('Title: @title', [
      '@title' => $form_state->getValue('title'),
    ]));
    $form_state->setRedirect('<front>');
  }

}

Le type MessengerInterface suffit au conteneur pour injecter le service messenger : nul besoin d'attribut #[Autowire] pour un service qui dispose d'un alias d'autowiring correspondant.

En résumé. Nous avons créé un module et une classe FormBase, lui avons donné un identifiant unique avec getFormId(), défini ses champs dans buildForm(), validé ceux-ci dans validateForm() avec setErrorByName(), traité les données dans submitForm() à l'aide du service messenger et de setRedirect(), puis exposé le tout avec une route _form. La réécriture pour Drupal 11 remplace Drupal Console par drush generate, la fonction supprimée drupal_set_message() par le service messenger, et ajoute les types stricts et les déclarations de type de retour.

Les formulaires sont partout dans Drupal, autant être à l'aise avec eux. Quel est le formulaire le plus délicat que vous ayez eu à construire ? Partagez-le dans les commentaires.