Rendre du json avec Twig

|Cas d'usage | faciliter et normaliser la gestion des données transmises à une API| |Niveau| Avancé|

Dans le cas d'un appel API récurrent (gestion d'un compte utilisateur sur un webservice client), il a été nécessaire de gérer la transmission d'un json légèrement différent selon les différents cas (mise à jour, création, suppression, statut du compte).

Le principal problème rencontré est que, selon la situation, les données à transmettre étaient issues soit d'un array, soit d'un objet User. D'autre part, certaines infos du json ne devaient être transmises que dans le cas d'une mise à jour (le mot de passe, par exemple, ne devait être transmis que s'il s'agissait d'une mise à jour, puisque les process de création de compte et de création de mot de passe sont distincts sur ce webservice).

En partant sur la solution la plus classique de stocker l'array de base dans une constante, il y aurait eu beaucoup de manipulation de l'array :

const BASE_ARRAY = [...];

private function prepareArray($user, $operation='create') {
    $body = self::BASE_ARRAY;
    $body['user']['userName'] = is_array($user) ? $user['email'] : $user->getEmail();
    // à faire pour la plupart des cellules

    switch ($operation) {
        case 'create':
            unset($body['user']['password'];

             break;
    }
    // etc etc
}

Afin d'éviter les problèmes de lisibilité, en se basant sur l'article sourcé en bas de page, le json a été stocké dans un template api-response.json.twig, le contenu lui étant transmis par des variables. Le principe est simple :

  1. Tout le json est stocké dans un template que le moteur de rendu se charge de gérer le rendu en amont de l'appel api
  2. Les données sont préparées en amont par un service dédié
  3. Avant l'appel, le service utilise le moteur de rendu Twig pour le rendu du template avec les données passées en variables.
  4. Lors de l'appel, le template rendu est utilisé comme body de la requête.

Dans le détail :

Le template

Le template .json.twig est littéralement juste la structure json vide :

{# mon_module/templates/base-user-array.json.twig #}
{% set name = is_array(user) ? user.name : user.getName() %}
{
    "user": {
        "email":{ is_array(user) ? user.username : user.getEmail() }}",
        "name": {
            "familyName": "{{ is_array(user) ? user.family_name : user.getFamilyName()  }}",
            "givenName": "{{ givenName }}",
        }
    },
    "title": "{{ is_array(user) ? user.title : user.getTitle }}",
    "active":  {{ status }},
    {% if password is not empty%}
        "password": "{{ password }}",
    {% endif %}
    "contact": [
        {
            "value": "{{ is_array(user) ? user.contact : user.getContact() }}",
            "type": "{{ is_array(user) ? user.contactType : user.getContactType()'}}"
        }
    ],
}

L'avantage est que l'on peut y traiter les données (user.name, user.getEmail) au lieu de le faire dans le php (les opérations sont strictement les même, mais ça allège la lecture de la classe), et que l'on peut lui passer des variables préparées utilisables telles qu'elles. On pourrait aussi préparer les données en amont via {% set %}, mais ça n'aurait pas forcément été un gros gain de lisibilité (à noter que le in_array() est une fonction twig custom rajoutée sur le projet).

La préparation des données et le rendu du template

Il s'agit la de php tout à fait classique, la partie intéressante étant le rendu du template :

private function prepareBody($user, $password = NULL) {
    $options = [
        'user' => $user,
        'password' => $password,
        'status' => $active ?? 'false',
        'theme_hook_original' => 'not-applicable',
    ];

    return twig_render_template(drupal_get_path('module', 'mon_module') . '/templates/idps-user.json.twig', $options);
}

Le principe est typique Drupal : on prépare un renderable array (avec le theme_hook "not-applicable"), puis à l'aide du moteur twig appelé via la fonction twig_render_engine(), on génère le rendu en y passant :

  • le path du template
  • le render array

l'appel API

  public function createIdpsUser(User $user, $password) {
     $options = [
         'headers' => $this->getRequestHeaders(),
         'body' => $this->prepareUser($user, $password),
    ];

     try {
          $response = $this->client->post($this->getUri(), $options);
     } 
     catch (ClientException $e) {
        // Do some magic here
     }
  }

Note importante :

Ceci ne fonctionnera que tant Twig est chargé (dés lors que le rendu est assuré par un thème), mais si vous deviez utiliser cette mécanique dans un contexte sans thème (envoi de mail via drush, tâche cron, tâche exécutée en front sur un site headless), il faut inclure manuellement le moteur Twig :

include_once \Drupal::root() . '/core/themes/engines/twig/twig.engine';

Source :

  1. https://www.jeffgeerling.com/blog/2019/rendering-twig-templates-programmatically-drupal-8