
Lorsque l'on souhaite utiliser Drupal pour gérer des données de sources externes, hormis les données chaudes, nous avons tout intérêt à les intégrer côté backend pour profiter du système d'affichage et de cache de Drupal. Votre client peut vous demander d'afficher ses points de vente sur une carte, de gérer un catalogue de produits ou encore de réaliser un annuaire.
Pour gérer ces cas différents, on peut se baser sur le même mécanisme : il faut synchroniser les données de l'api avec des entités miroir dans la base Drupal, de façon périodique. L'affichage de ces données est ensuite géré de manière native par Drupal en utilisant les vues, les modes d'affichage et les templates. Cet article a pour but de passer en revue les outils à notre disposition dans l'écosystème Drupal pour réaliser cette tâche.
Dans un premier temps, il va falloir créer la structure de données en choisissant le type d'entité à créer : type de contenu, vocabulaire, type d'entité custom, etc. Tout dépend de l'usage que souhaitez en faire mais le choix de l'entité porteuse de l'info ne diffère pas de la façon classique de concevoir un site en Drupal pour gérer des contenus uniquement contribués. Ce choix étant totalement lié au contexte métier, je ne m'attarderais pas dessus.
Le développement de cette fonctionnalité va s'articuler autour d'un module custom qui aura pour mission de :
Pour cloisonner les développements, je conseille de réaliser un module custom qui englobera les développements en lien avec cette synchronisation. On peut évidemment découper davantage la solution, en isolant les fonctionnalités dans plusieurs modules custom pour faciliter la maintenance et la ré-utilisabilité. Tout dépend du contexte de développement : complexité métier, expérience des développeurs, etc.
La première étape va consister à créer les types d'entités nécessaires à notre synchronisation. Pour les types natifs (types de contenus/vocabulaires), la manipulation se fera par l'interface d'administration de manière conventionnelle. Si vous avez besoin de types d'entités personnalisés, une commande drush est disponible et vous permettra de créer un module dédié.
mise en forme code : drush generate content-entity

L'ajout des champs, y compris pour une entité custom, se fait par l'administration. Je conseille dans la mesure du possible d'utiliser des champs basiques (texte court non formaté, entier, etc.) et de ne pas utiliser de champs formatés. Cela permet de simplifier la création/mise à jour des champs par le code.
Pour gérer l'automatisation de la synchronisation, il faudra créer une ou plusieurs commandes drush. Cela permettra de pouvoir lancer l'import en ligne de commande depuis n'importe quel environnement et donc de faire appel à ces commandes dans la CRONTAB. Avec la configuration xDebug adéquate, cette commande pourra être exécutée en pas à pas pour faciliter les étapes de développement.
Voici un code minimal pour la création d'une commande Drush, elle pourra être lancée par la commande :
drush api-import:synchronize
<?php
namespace Drupal\my_api_import_module\Commands;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drush\Commands\AutowireTrait;
use Drush\Commands\DrushCommands;
final class MyDrushCommand extends DrushCommands
{
use AutowireTrait;
public function __construct(private readonly EntityTypeManagerInterface $entityTypeManager)
{
parent::__construct();
}
/**
* Commande de synchronisation des données de l'api.
*
* @command api-import:synchronize
* @aliases api-sync
* @usage api-import:synchronize
*/
public function synchronizeData(): void
{
// Code métier.
// Appels à des services.
}
}
La connexion avec l'api externe sera réalisée dans une classe de service. L'objectif est d'isoler la connection des autres traitements métiers pour obtenir un service purement technique. On pourra y découper la gestion des headers, si elle est complexe et y gérer l'authentification mais le traitement des données sera réalisé dans une autre classe.
<?php
namespace Drupal\my_api_import_module;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
class ApiClient
{
public function __construct(private readonly ClientInterface $httpClient)
{
}
public function request()
{
try {
$response = $this->httpClient->request('GET', 'https://api.externalsite.com/endpoint', ['headers' => ['Accept' => 'application/json', 'Content-type' => 'application/json',],]);
return json_decode($response->getBody()->getContents(), true);
} catch (RequestException $e) {
// Gestion de l'erreur
// Ajout dans monolog
$e->getResponse();
}
return FALSE;
}
}
Toutes les api ne proposent pas de pagination, par conception ou à cause d'une quantité de données faible mais dans les autres cas, vous devrez faire en sorte de parcourir toutes les pages de résultats pour récupérer l'intégralité des données correspondant à la requête. Une bonne api vous fournira le nombre de données disponibles pour toutes les pages et le nombre de pages disponibles, voire le nombre de résultats de la page suivante.
Si ces informations ne sont pas présentes, le code devra parcourir les pages tant qu'elles proposent des résultats.
<?php
$queue = \Drupal::queue('my_module_import');
$page = 1;
$hasMore = TRUE;
while ($hasMore) {
$response = $this->httpClient->request('GET', 'https://api.externalsite.com/endpoint', ['query' => ['page' => $page, 'per_page' => 50],]);
$payload = json_decode((string)$response->getBody(), TRUE);
foreach ($payload['data'] as $item) {
// Ajout des données dans la queue, voir étape suivante
$queue->createItem($item);
}
$hasMore = !empty($payload['items_on_next_page']);
$page++;
}L'utlisation de la queue permet de résoudre des problématiques qui interviennent lorsque la quantité de données à importer est conséquente. Une fois les données ajoutées à la queue, chaque élément sera traité de manière indépendante avec possibilité de reprise en cas d'interruption. L'utilisation de ce mécanisme permet également de séparer les appels http, pour récupérer les données, de la persistance des données. On comprend donc que cette brique est à mettre en place uniquement lorsque la volumétrie est importante. Pour traiter peu de données, un traitement synchrone sera amplement suffisant et simplifiera l'application.
On a vu à l'étape précédente que l'ajout des éléments se fait de manière simpliste. Pour exploiter ces données, l'utilisation du cron est adaptée puisqu'elle va nous permettre de traiter en tâche de fond les données récupérées de l'API. Pour cela, on créer un classe qui étend QueueWorkerBase. Cette classe sera scrutée par la tâche cron qu'il faudra configurer pour être lancée la nuit par exemple. C'est l'annotation @QueueWorker qui va permettre cette automatisation. Dans l'exemple suivant, le paramètre cron = {"time" = 30} définit le temps d'exécution de la Queue à chaque déclenchement du cron.
<?php
// @QueueWorker(
// id = "my_module_import",
// title = "Import distant",
// cron = {"time" = 30}
// )
class MyModuleImportWorker extends QueueWorkerBase
{
public function processItem($item): void
{
// recherche d'une entité existante via un identifiant distant
if ($exists) {
// logique de mise à jour
} else {
// logique de création
}
// persistance de l'entité
}
}Le cron devra être configuré sur la plateforme accueillant le drupal pour gérer à la fois la récupération des données et leur traitement dans un second temps. Il faudra créer deux entrées dans la crontab, une première pour déclencher la commande drush et une seconde pour traiter la queue.
Dans la partie sur la mise en place de la Queue, on a pu voir que le cron déclenchait la classe parce qu'elle était annotée. Le temps d'exécution défini dans cette annotation est à calibrer suivant vos besoins. Dans certains cas, une exécution unique de 30 secondes suffira mais pour beaucoup de données, il sera intéressant de faire un découpage. On pourra alors mettre en place dans la crontab un déclenchement toutes les 5 min pendant une heure. L'idéal étant de connaitre le temps d'exécution théorique de la synchronisation complète.
On a pu voir pourquoi et comment mettre en place un système de synchronisation de données externes sur un site en Drupal. La solution proposée n'est pas unique, il en existe d'autres. L'important est de calibrer la solution par rapport à la quantité de données, à la structure de l'api distante et au taux de rafraichissement des données attendu.