Créer un formulaire personalisé en Drupal 8

Créer des formulaires fait partie de la vie quotidienne de tout développeur Drupal. Les formulaires se présentent sous forme de vecteurs tant sous Drupal 7 que sous Drupal 8. La différence réside dans le fait que  sous Drupal 8 ces vecteurs sont définis dans des classes alors qu'en Drupal 7 ils sont créés dans des fonctions.

Dans ce post nous allons créer un  formulaire personnalisé avec deux champs, un champs de type text et un autre de type checkbox, nous allons valider ces champs, les afficher dans un message et enfin rediriger l'utilisateur vers la page principale.

Vous pouvez télécharger le code de ce module ici.

Voici un aperçu du formulaire:

drupal 8 custom form

Vous pouvez voir la structure de ce module ci-dessous:

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

2 directories, 3 files

Dans un premier temps nous allons créer ce formulaire étape par étape, ensuite nous utiliserons Drupal Console pour le faire plus rapidement et facilement.

1. Créer le squelette du module
2. Créer la classe contrôleur du formulaire et donner un identifiant à ce dernier
3. Construire le formulaire
4. Valider le formulaire
5. Traiter les données
6. Créer une route vers le formulaire
7. Refaire tout ce qui précède plus facilement avec Drupal Console

Sous Drupal 8, tout formulaire est défini par une classe controller qui implémente l'interface \Drupal\Core\Form\FormInterface laquelle détermine quatre méthodes:

getFormId() - Donne un identifiant unique (ID) au formulaire
buildForm() - Se lance lorsque l'utilisateur appelle le formulaire. Elle construit le vecteur $form et les champs requis.
validateForm() -  Se lance lorsque le formulaire est envoyé. Elle est utilisée pour vérifier les valeurs introduites et optionellement déclenche une erreur.
submitForm() -  Réalise le traitement des données du formulaire si celui-ci ne comporte pas d'erreur.

1. Créer le squelette du module

Comme toujours nous allons utiliser Drupal Console pour créer le module. Ce module s'appellera "ex81". N'oubliez pas de l'installer une fois créé.

Pour ce faire nous utilisons la commande suivante:

drupal generate:module  --module="ex81" \
--machine-name="ex81" \
--module-path="modules/custom" \
--description="Example of a simple custom form" \
--core="8.x" \
--package="Custom" \
--uri="http://default" \
--no-interaction

Une fois le module créé, nous l'installons avec la commande: drupal module:install ex81

2. Créer la classe contrôleur du formulaire

Nous allons créer un nouveau fichier PHP que nous nommerons HelloForm.php dans le répertoire src/Form. Dans ce fichier nous créons une classe contrôleur HelloForm qui étends la classe abstraite FormBase (qui à son tour implémente l'interface FormInterface).

Dans cette classe nous créons la méthode getformId() qui définit un identifiant unique pour notre formulaire.

<?php

namespace Drupal\ex81\Form;

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

class HelloForm extends FormBase {

  /**
   * Returns a unique string identifying the form.
   *
   * The returned ID should be a unique string that can be a valid PHP function
   * name, since it's used in hook implementation names such as
   * hook_form_FORM_ID_alter().
   *
   * @return string
   *   The unique string identifying the form.
   */
  public function getFormId() {
    return 'ex81_hello_form';
  }

}

3. Construire le formulaire

À présent nous ajoutons à cette classe une autre méthode appelée buildForm() qui définira les champs text et checkbox du formulaire. Nous ajoutons aussi un champ de type description pour afficher un petit message au-dessus du formulaire.

Pour terminer nous ajoutons à la fin de la méthode un champ de type submit et nous revoyons le vecteur ainsi créé.

  /**
   * Form constructor.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   The form structure.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $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. Note that the title must be at least 10 characters in length.'),
      '#required' => TRUE,
    ];

    $form['accept'] = array(
      '#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'),
    );


    // Group submit handlers in an actions element with a key of "actions" so
    // that it gets styled correctly, and so that other modules may add actions
    // to the form. This is not required, but is convention.
    $form['actions'] = [
      '#type' => 'actions',
    ];

    // Add a submit button that handles the submission of the form.
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;

  }

4. Valider le formulaire

Pour valider le formulaire nous avons simplement besoin de la méthode validateForm() de l'interface  \Drupal\Core\Form\FormInterface.

Dans notre exemple nous allons générer une erreur si la longueur du titre es inférieur à 10 caractères et aussi si la checkbox n'a pas été coché. Les valeurs introduites sont accessibles grâce à l'objet et la méthode $form_state->getValue('key') où "key" est le nom de l'élément du formulaire auquel nous voulons avoir accès.

Nous générons une erreur grâce à la méthode setErrorByName() de l'objet $form_state.

  /**
   * Validate the title and the checkbox of the form
   * 
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   * 
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);

    $title = $form_state->getValue('title');
    $accept = $form_state->getValue('accept');

    if (strlen($title) < 10) {
      // Set an error for the form element with a key of "title".
      $form_state->setErrorByName('title', $this->t('The title must be at least 10 characters long.'));
    }

    if (empty($accept)){
      // Set an error for the form element with a key of "accept".
      $form_state->setErrorByName('accept', $this->t('You must accept the terms of use to continue'));
    }

  }

5. Traiter les données du formulaire

Il est temps à présent de traiter les données de notre formulaire. Pour ce faire nous utilisons la méthode submitForm() ou la "submission handler method" en anglais.

C'est ici que nous allons pouvoir sauvegarder les valeurs dans la base de données, les envoyer vers une API externe ou encore les utiliser dans un service. Dans notre cas, nous allons simplement les afficher et rediriger l'utilisateur vers la page principale. Pour rediriger l'utilisateur nous allons utiliser la méthode setRedirect() de l'objet $form_state.

Dans le cas où vous voudriez sauvegarder ces valeurs dans le système de configuration, vous devriez étendre la classe ConfigFormBase comme nous l'avons vu dans ce post.

  /**
   * Form submission handler.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {

    // Display the results.
    // Call the Static Service Container wrapper
    // We should inject the messenger service, but its beyond the scope of this example.
    $messenger = \Drupal::messenger();
    $messenger->addMessage('Title: '.$form_state->getValue('title'));
    $messenger->addMessage('Accept: '.$form_state->getValue('accept'));

    // Redirect to home.
    $form_state->setRedirect('<front>');

  } 

Voilà, nous pouvons maintenant sauvegarder notre contrôleur.

6. Créer une route vers le formulaire

Une fois le contrôleur créé, nous allons à présent créer la route qui permettra à nos utilisateurs d'accéder au formulaire grâce au fichier de routing.

Si nous l'avions pas créé avant, nous créons le fichier ex81.routing.yml à la racine de notre module. Dans ce fichier nous allons ajouter une route qui mènera vers le contrôleur de     notre formulaire. Dans ce fichier nous n'allons pas utiliser la clé _controller, mais la clé _form pour que Drupal puisse utiliser le service de création de formulaire lors de la requête.

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

Le code ci-dessus ajoute une nouvelle route qui associe le chemin /ex81/helloform au contrôleur de notre formulaire Drupal\ex81\Form\HelloForm.

Nous pouvons maintenant naviguer sur notre site et tester notre formulaire!

Récapitulons.

1. Nous avons créé le module "ex81" avec la commande drupal generate:module de Drupal Console
2. Nous avons ensuite créé la classe contrôleur HelloForm de notre formulaire dans le répertoire src\Form. Cette classe étends la  classe abstraite FormBase qui implémente l'interface  FormInterface.
3. Nous avons défini un identifiant unique pour notre formulaire avec la méthode getFormId().
4. Nous avons créé les champs de notre formulaire avec la méthode buildForm().
5. Pour valider le formulaire nous avons utilisé la méthode validateForm() et généré les possibles erreurs grâce à la méthode setErrorByName() de l'objet $form_state.
6. Nous avons traité les données du formulaire avec la méthode submitForm() et ensuite rediriger l'utilisateur vers la page principale avec la méthode setRedirect() de l'objet $form_state.
7. Nous avons créé une route vers notre formulaire en utilisant la clé _form

7. Bonus: Créer un formulaire personalisé avec Drupal Console

Nous allons créer le même formulaire comme ci-dessus mais dans un autre module que nous appellerons 'ex82'.

drupal generate:module  --module="ex82" \
--machine-name="ex82" \
--module-path="modules/custom" \
--description="Example of a simple custom form" \
--core="8.x" \
--package="Custom" \
--uri="http://default" \
--no-interaction

A présent nous allons créer le formulaire grâce à une simple commande de Drupal Console: drupal generate:form

drupal generate:form  \
--module="ex82" \
--class="HelloForm" \
--form-id="ex82_hello_form" \
--services='messenger' \
--inputs='"name":"description", "type":"item", "label":"Description", "options":"", "description":"Please enter the title and accept the terms of use of the site.", "maxlength":"", "size":"", "default_value":"", "weight":"0", "fieldset":""' \
--inputs='"name":"title", "type":"textfield", "label":"Title", "options":"", "description":"Enter the title of the book. Note that the title must be at least 10 characters in length.", "maxlength":"64", "size":"64", "default_value":"", "weight":"0", "fieldset":""' \
--inputs='"name":"accept", "type":"checkbox", "label":"I accept the terms of use of the site", "options":"", "description":"Please read and accept the terms of use", "maxlength":"", "size":"", "default_value":"", "weight":"0", "fieldset":""' \
--path="/ex82/helloform" \
--uri="http://default" \
--no-interaction

Cette commande va générer deux fichiers: HelloForm.php and ex82.routing.yml, comme vous pouvez le constater, Drupal Console a générer presque tout le formulaire.

Jetons un coup d'oeil sur le fichier HelloForm.php

<?php

namespace Drupal\ex82\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Messenger\MessengerInterface;

/**
 * Class HelloForm.
 */
class HelloForm extends FormBase {

  /**
   * Drupal\Core\Messenger\MessengerInterface definition.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;
  /**
   * Constructs a new HelloForm object.
   */
  public function __construct( MessengerInterface $messenger) {
    $this->messenger = $messenger;
  }

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('messenger')
    );
  }

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

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['description'] = [
      '#type' => 'item',
      '#title' => $this->t('Description'),
      '#description' => $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. Note that the title must be at least 10 characters in length.'),
      '#maxlength' => 64,
      '#size' => 64,
    ];
    $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'),
    ];
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Display result.
    foreach ($form_state->getValues() as $key => $value) {
      drupal_set_message($key . ': ' . $value);
    }

  }

}

Vous pouvez voir que les deux méthodes getFormId() et buildForm() sont complètes et nous n'avons rien à y ajouter.

Ensuite vous pouvez constater que nous avons volontairement injecter le service messenger service depuis la commande de Drupal Console (--services='messenger' \),  cela nous permettra d'éviter d'appeler le 'Static Service Container wrapper' \Drupal::messenger(). Nous l'utiliserons dans la méthode buildForm() plus tard.

Nous devons maintenant ajouter le code nécessaire pour la validation et le traitement du formulaire avec les deux méthodes:
- validateForm()
- buildForm()

Vous pouvez copier le même code de la méthode validateForm() décrite plus haut dans la section 4.

  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);

    $title = $form_state->getValue('title');
    $accept = $form_state->getValue('accept');

    if (strlen($title) < 10) {
      // Set an error for the form element with a key of "title".
      $form_state->setErrorByName('title', $this->t('The title must be at least 10 characters long.'));
    }

    if (empty($accept)){
      // Set an error for the form element with a key of "accept".
      $form_state->setErrorByName('accept', $this->t('You must accept the terms of use to continue'));
    }

  }

Pour la méthode buildForm() nous utiliserons le même code mais avec un petit changement puisque nous avons déjà injecter le service messenger.

 public function submitForm(array &$form, FormStateInterface $form_state) {

    $this->messenger->addMessage('Title: '.$form_state->getValue('title'));
    $this->messenger->addMessage('Accept: '.$form_state->getValue('accept'));

    // Redirect to home
    $form_state->setRedirect('<front>');
    return;
  }

Et voilà !!! C'est aussi simple que ça! Si nous créons nos formulaires avec Drupal Console, nous allons simplement avoir besoin de nous concentrer sur les méthodes de validation et de traitement, nous laissons le reste du code à DC. Drupal Console est définitivement un outil indispensable à tout développeur Drupal.