POST des données Webform vers une API REST externe

Dans cet article

Si vous avez un besoin de formulaire un tant soit peu complexe dans Drupal, vous utilisez très certainement le module Webform. C'est le cas du site de l'un de mes clients, dont l'ensemble des formulaires sont créés via ce module. Ceux-ci sont multiples sur le site : formulaire de contact, formulaire d'inscription newsletter ou encore formulaire de candidature. Le besoin s'est fait sentir de remonter les informations de ces formulaires vers Sugar CRM, ou plus exactement "de créer des leads dans Sugar Market grâce aux informations issus des webforms".

Il nous faut donc une manière de récupérer les informations soumises dans un formulaire du module Webform et de les transmettre à l'API de Sugar CRM dans le format souhaité. Egalement, puisque plusieurs formulaires vont renvoyer leurs informations, nous devrons être le plus générique possible, et offrir une configuration permettant de faire le lien entre le champ webform et la donnée dans l'API.

La méthode est alors toute trouvée : la création d'un webform handler : une classe capable d'effectuer un traitement sur les données envoyées dans le formulaire.

Il serait tentant d'utiliser un hook_form_alter pour altérer le formulaire webform comme un formulaire Drupal classique. La situation est à la fois plus simple et plus complexe car Webform fournit des points d'entrée et des outils pour vos configurations, comme ici le plugin @WebformHandler permettant d'agir à la soumission de données, mais aussi de disposer de configurations propres.

A nouveau, c'est le système de plugin de Drupal qui vient à notre rescousse. Dans la configuration de votre formulaire webform ; dans l'onglet handler, un système nommé plugin manager va découvrir l'ensemble des plugins de type @WebformHandler déclarés dans le code et vous proposer de les affecter au webform. Ainsi, lorsque celui-ci sera validé, l'ensemble des gestionnaires (handlers) en question seront appelés un à un pour effectuer leurs actions spécifiques sur les données soumises. L'un peut sauvegarder les résultats, l'autre envoyer un email de confirmation à l'utilisateur ou encore un email de notification à l'administrateur. Un autre enfin, celui que nous allons créer, va envoyer les données à une API externe.

Ajout d'un handler à un webform via la UI.
Ajout d'un gestionnaire (handler) à un webform via la UI.

Création d'un handler minimaliste

La première chose à faire est de créer un plugin de type @WebformHandler. Pour cela :

  • Nous utiliserons l'annotation @WebformHandler sur une nouvelle classe d'un module custom
  • Cette classe se trouve placée dans le dossier src/Plugin/WebformHandler de notre module
  • La classe étendra WebformHandlerBase
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
 
namespace Drupal\custommodule\Plugin\WebformHandler;
 
use Drupal\webform\Annotation\WebformHandler;
use Drupal\webform\Plugin\WebformHandlerBase;
 
/**
 * Webform submission remote post handler.
 *
 * @WebformHandler(
 *   id = "handler_sugar_market",
 *   label = @Translation("SugarMarket new Contact connector"),
 *   category = @Translation("External"),
 *   description = @Translation("Inserts webform submission as a new SugarMarket Contact."),
 *   cardinality = \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_SINGLE,
 *   results = \Drupal\webform\Plugin\WebformHandlerInterface::RESULTS_PROCESSED,
 *   submission = \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_OPTIONAL,
 *   tokens = TRUE,
 * )
 */
class SugarMarketWebformHandler extends WebformHandlerBase {
 
}
Retrouvez les propriétés disponibles pour cette annotation dans la classe Drupal\webform\Annotation\WebformHandler

Nous remarquons dans cette définition les quelques propriétés suivantes :

  • id : l'ID du handler, c'est assez évident
  • label : là aussi, pas vraiment besoin d'explication
  • category : permet de ranger les gestionnaires par catégorie dans la UI d'admin. Un peu comme pour les sections de la page module finalement
  • description : assez évident aussi !
  • cardinality : combien de handler de ce type vous pouvez attacher à un même webform. Suivant la nature de votre gestionnaire, il peut faire sens de limiter à un seul, comme ici.
  • results : indique si les résultats vont être utilisés par le handler. En l'occurrence ici oui puisque nous allons envoyer nos résultats à une API externe.
  • submission : indique si le handler a besoin que les résultats soient sauvegardés en base de données pour fonctionner. Ici évidemment non. Il serait même envisageable de ne se servir du webform que comme d'un formulaire pour l'envoi vers un système externe, sans rien sauvegarder en interne sur notre site
  • token : si le support des tokens s'avère nécessaire

A partir de là, notre webform devient disponible dans la UI.

Add handler from the UI
Add handler from the UI

La classe WebformHandlerBase que nous étendons comprend un grand nombre de méthodes que nous pouvons surcharger. En particulier, postSave() nous permettra d'agir sur la soumission après sa sauvegarde (mais avant les hook insert et update). Cela se fait par l'usage de la méthode au sein de la classe de notre gestionnaire.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * {@inheritdoc}
 */
public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE) {
  // Get the submitted data as an array.
  $submission = $webform_submission->toArray(TRUE);
 
  // Build data from the submissions.
  $data = [
    'crm_type' => 'Contact',
    'first_name' => $submission['data']['firstname'],
    'last_name' => $submission['data']['lastname'],
    'email' => $submission['data']['email'],
    'country' => 'France',
  ];
 
  // Send via an appropriate service.
  $this->sugarMarketService->postContact($data);
}

Et c'est tout !

Lors de la soumission du formulaire, si bien entendu le handler est utilisé dans le webform, cette méthode va être appelée automatiquement. Les données de soumission du formulaire sont récupérées et transmises ici à un service que nous avons créé pour maintenant la lisibilité du code.

Quelques configurations supplémentaires

Dans le paragraphe précédent, nous avons découvert le code minimal pour le fonctionnement d'un gestionnaire de soumission de formulaire. Cela dit, il est évidemment possible d'aller plus loin en offrant à ce gestionnaire la possibilité d'être configuré.

Plus clairement, en reprenant notre exemple précédent, nous avons hardcodé la correspondance entre le champ de notre formulaire et le champ correspondant de l'API SugarMarket. Cela fonctionne parfaitement pour notre projet et c'est suffisant ! Pour autant, si le projet devait être plus générique, nous pourrions proposer une configuration permettant à l'administrateur de choisir quelle donnée, parmi l'ensemble des champs du webform, associer à chaque champ de l'API.

La classe WebformHandlerBase que nous avons étendu contient des tas de méthodes pour un grand nombre de cas d'usage. Je ne peux que vous conseiller de jeter un œil à son interface pour découvrir l'étendue des possibilités.

Nous allons pour cela devoir utiliser plusieurs méthodes différentes :

  1. defaultConfiguration() : cette méthode permet de définir l'ensemble des configurations utilisées par notre gestionnaire ainsi que lui attribuer une valeur par défaut
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    /**
     * {@inheritdoc}
     */
    public function defaultConfiguration() {
      return [
        // Options
        'contact_firstname' => '',
        'contact_lastname' => '',
        'contact_email' => '',
        'contact_country' => '',
        'contact_language' => '',
        'contact_type' => 'Contact',
      ];
    }
  2. buildConfigurationForm() : cette méthode permet la construction d'un formulaire de configuration qui sera disponible en admin.
    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
    
    /**
     * {@inheritdoc}
     */
    public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
      // Retrieve a list of all fields configured in the webform build.
      $options = $this->buildWebformFieldListAsSelectOptions();
     
      $form['submission_data'] = [
        '#type' => 'details',
        '#title' => $this->t('Submission data mapping'),
        '#description' => $this->t('Map the webform field to SugarMarket Contacts field data'),
      ];
      $form['submission_data']['contact_firstname'] = [
        '#type' => 'select',
        '#title' => $this->t('Contact firstname'),
        '#default_value' => $this->configuration['contact_firstname'],
        '#options' => $options,
        '#empty_value' => '',
        '#empty_option' => $this->t('- Do not send -')
      ];
     
      // Continue with all necessary fields.
      // ....
     
      return $this->setSettingsParents($form);
    }
    Formulaire de configuration du gestionnaire
    Formulaire de configuration du gestionnaire
  3. submitConfigurationForm() : sauvegarde de la configuration. Heureusement, il n'y a quasiment rien à faire grâce aux méthodes d'aide de Webform.
    1
    2
    3
    4
    5
    6
    7
    8
    
    /**
     * {@inheritdoc}
     */
    public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
      parent::submitConfigurationForm($form, $form_state);
      // Helper method made by Webform for us: saves the configurations.
      $this->applyFormStateToConfiguration($form_state);
    }
  4. getSummary() : construit une prévisualisation des configurations enregistrées à afficher sur la page de listing des handlers associés à un webform.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    /**
     * {@inheritdoc}
     */
    public function getSummary() {
      $configuration = $this->getConfiguration();
      $settings = $configuration['settings'];
     
      return [
          '#settings' => $settings,
        ] + parent::getSummary();
    }
  5. postSave() : nous pouvons désormais modifier notre méthode d'envoi des données pour prendre en compte nos modifications.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    /**
     * {@inheritdoc}
     */
    public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE) {
      $submission = $webform_submission->toArray(TRUE);
     
      $data = [
        'crm_type' => $this->configuration['contact_type']
      ];
     
      if (!empty($this->configuration['contact_firstname'])) {
        $data['first_name'] = $this->getSubmissionValue($submission['data'], $this->configuration['contact_firstname']);
      }
     
      // Continue with other fields....
     
      $this->sugarMarketService->postContact($data);
    }
  6. buildWebformFieldListAsSelectOptions() : notre premier helper. Nous l'avons utilisé dans la méthode buildConfigurationForm. Il permet de créer la liste des champs comme options pour nos select.
    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
    
    /**
     * Builds a key => name list of the fields in the webform.
     *
     * This is made to be used as #options in a select list to choose an
     * element among the webform configured fields.
     *
     * @return array
     */
    protected function buildWebformFieldListAsSelectOptions() {
      $webform = $this->getWebform();
      $elements = $webform->getElementsInitializedFlattenedAndHasValue('view');
      foreach ($elements as $key => $element) {
        $title = $element['#admin_title'] ?: $element['#title'] ?: $key;
        if (!empty($element['#webform_composite_elements'])) {
          foreach ($element['#webform_composite_elements'] as $subkey => $subelement) {
            if (!isset($subelement['#access']) || $subelement['#access']) {
              $subtitle = $subelement['#admin_title'] ?: $subelement['#title'] ?: $subkey;
              $options["$key::$subkey"] = "$title - $subtitle ($key)";
            }
          }
        } else {
          $options[$key] = "$title ($key)";
        }
      }
      return $options;
    }
  7. getSubmissionValue() : un second helper utilisé dans le nouveau postSave pour récupérer la valeur d'un champ depuis la clef du champ. Cette méthode permet d'aller récupérer en profondeur une valeur, par exemple extraire le pays en particulier depuis un champ complexe type adresse.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    /**
     * Get the submission value for a given complex key set.
     * @param $submissions
     * @param $keys
     *
     * @return mixed
     */
    protected function getSubmissionValue($submissions, $keys) {
      $split = explode('::', $keys);
      $value = $submissions;
      $index = 0;
      while (is_array($value)) {
        $value = $value[$split[$index]];
        $index++;
      }
      return $value;
    }

Voilà qui est fait !!

C'était un peu long, mais nous avons montré comment créer une classe capable de traiter les données soumises via un webform. Nous avons aussi montré comment créer une configuration pour notre handler, notamment dépendant des champs définis au sein de notre formulaire, ce qui était la partie la plus tricky de ce post.

Vous avez un autre usage des gestionnaires de soumission pour les webforms ? Dites-le moi en commentaire :)

Commentaires

Liliana H.

Bonjour Dominique !

Super pour ton article, il y en a assez peu sur ce type d'usage de Webform !
Dans mon cas, on souhaite utiliser Webform comme première étape d'un "tunnel d'achat". Webform envoie à un module de paiement externe les nom, prénom, adresse e-mail et somme à payer de l'utilisateur. Nous hésitons sur la marche à suivre, on aimerait sauvegarder la soumission du form et être redirigé vers le module externe.
Les 2 solutions envisagées sont :
• remote_post et redirection à la main car il transmet bien les données à mon module externe mais ne redirige pas vers la suite du process d'achat.
• confirmation url et enregistrement de la soumission avant d'être redirigé sur la page du module externe (données envoyées en GET)

Selon toi quel serait le meilleur système ?

Merci 🙂

Bonjour,

Difficile de décider en réfléchissant deux minutes comme ça, mais instinctivement envoyer des données en GET me parait bizarre et le remote handler est là pour ça. Donc j'aurais tendance à proposer la solution 1: l'usage du remote_post handler de webform pour l'envoi de la donnée et une redirection ensuite.

  1. Soit vous utilisez le handler remote_post présent dans webform s'il convient à votre besoin, et effectuez la redirection via un hook type hook_webform_handler_invoke_post_save_alter
  2. Soit vous étendez ce handler avec une classe custom si vous avez besoin de modifier la façon dont sont postées les données, et dans ce cas, vous pouvez effectuer la redirection en fin de méthode postSave avec un truc du genre $form_state->setRedirect(...)

Qu'en pensez-vous ?

Liliana H.

Oui c'est bien ce que je comptais faire (solution numéro 1 avec le hook_webform_handler_invoke_post_save_alter)
Merci pour la réponse rapide et précise !

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