Un module drupal pour utiliser l'API Sirene entreprise.data.gouv.fr dans un formulaire

Mardi 13 Juillet 2021

Voici le code d'un module en cours de contribution sur drupal.org. Avant de poursuivre, nous libérons le code et la démarche.

Capture d'écran du site api.adresse.gouv.fr

Le projet

A l'occasion de la refonte du formulaire de syndicalisation de la CGT, nous avons souhaité proposer aux utilisateurs, une simplification de leur parcours. L'idée est ici de prendre soin d'eux en leur faisant gagner du temps de saisie de leurs informations. En effet, lors du processus, le formulaire leur demande de saisir les informations concernant l'identité de leur employeur.

S'ils connaissent son SIREN ou SIRET, les utilisateurs peuvent remplir le champ concerné - Les informations concernant l'identité et l'adresse de leur employeur seront automatiquement proposées et renseignées en interrogeant l'API de la Plateforme ouverte des données publiques françaises.

S'ils connaissent la dénomination sociale de l'employeur, les utilisateurs remplissent le champ concerné - Les informations concernant l'adresse de leur employeur seront automatiquement proposées et renseignées en interrogeant l'API de la Plateforme ouverte des données publiques françaises.

L'objectif du module

Le module est disponible ici : 
https://www.drupal.org/project/api_sirene_open_data_france -

Le code est disponible ici : 
https://git.drupalcode.org/project/api_sirene_open_data_france/commit/4d7abc1 -

Le module peut servir à simplifier n'importe quelle inscription nécessitant des informations concernant les entreprises domiciliées en France. 

Nous allons prochainement l'utiliser dans le cadre de l'inscription de professionnels sur un site d'offres d'emploi. Seuls les utilisateurs représentant un employeur sont autorisés à déposer une offre d'emploi. Les cas d'usage sont multiples. Le connecteur à l'API de la plateforme Open Data France fonctionne très simplement et les délais de réponse sont très rapides.

L'API sirene

https://entreprise.data.gouv.fr/api_doc/sirene
Dans le cadre du Service Public de la Donnée, de nombreux jeux de données sont ouverts. La base de données SIRENE est consultable librement et gratuitement !

Ce service interroge l’API Sirene développée par Étalab.
Ce site web et son API sont open-source : vous pouvez télécharger le code sur GitHub.
Les données de référence SIRENE sont disponibles sur Data.gouv.fr.

La démo

Autocompletion à partir du SIREN ou du SIRET de l'entreprise : 

Autocompletion à partir de la dénomination sociale de l'entreprise :

Création de la route

On commence par ajouter le route pour l'Ajax callback dans le fichier .routing.yml du module :

bluedrop_new.entreprise_data_autocomplete:
  path: '/client-action/entreprise-data-autocomplete'
  defaults:
    _controller: '\Drupal\bluedrop_entreprise\Controller\EntrepriseAjaxController::entreprise_data_autocomplete'
    _title: 'Search'
    _format: json
  requirements:
    _access: 'TRUE'

Le fichier manager

On crée un fichier Manager (à étendre), pour l'instant il ne contient que les méthodes nécessaires pour la recherche des adresses et l'autocomplétion des champs.
EntrepriseDataApiManager.php

<?php

namespace Drupal\bluedropfr_syndicalisation_new;

use Drupal\Component\Serialization\Json;
use GuzzleHttp\Exception\RequestException;
/**
 * Basic manager of module.
 */
class EntrepriseDataApiManager {
  /**
   * The request url of the API.
   */
  const API_URLv1 = 'https://entreprise.data.gouv.fr/api/sirene/v1';
  const API_URLv3 = 'https://entreprise.data.gouv.fr/api/sirene/v3';
 
  public $client;
  /**
   * Constructor.
   */
  public function __construct() {
    if (!function_exists('curl_init')) {
      $msg = 'Entreprise Data API requires CURL module';
      \Drupal::logger('bluedropfr_syndicalisation_new')->error($msg);
      return;
    }
    $this->client = \Drupal::httpClient();
  }

  /**
   * Do CURL request with authorization.
   *
   * @param string $resource
   *   A request action of api.
   * @param string $method
   *   A method of curl request.
   * @param Array $inputs
   *   A data of curl request.
   *
   * @return array
   *   An associate array with respond data.
   */
  private function executeCurl($resource, $method, $inputs, $api) {
    if (!function_exists('curl_init')) {
      $msg = 'Entreprise Data API requires CURL module';
      \Drupal::logger('bluedropfr_syndicalisation_new')->error($msg);
      return NULL;
    }
    if(is_array($resource)){
      $api_url =  $api;
      foreach ($resource as $res){
        $api_url.="/".$res;
      }
    }else{
      $api_url = $api . "/" . $resource;
    }
    
    $options = [
      'headers' => [
        'Content-Type' => 'application/json'
      ],
    ];

    if (!empty($inputs)) {
      
      if($method == 'GET'){
        $api_url.= '?' . self::arrayKeyfirst($inputs) . '=' . array_shift($inputs);
        foreach($inputs as $param => $value){
            $api_url.= '&' . $param . '=' . $value;
        }
      }else{
        //POST request send data in array index form_params.
        //$options['body'] = $inputs;
      }
    }

    try {
      $clientRequest = $this->client->request($method, $api_url, $options);
      $body = $clientRequest->getBody();
    } catch (RequestException $e) {
      \Drupal::logger('bluedropfr_syndicalisation_new')->error('Curl error: @error', ['@error' => $e->getMessage()]);
    }

    return Json::decode($body);
  }

  /**
   * Get Request of API.
   *
   * @param string $resource
   *   A request action.
   * @param string $input
   *   A data of curl request.
   *
   * @return array
   *   A respond data.
   */
  public function curlGet($resource, $inputs, $api) {
    return $this->executeCurl($resource, "GET", $inputs, $api);
  }

  /**
   * Post Request of API.
   *
   * @param string $resource
   *   A request action.
   * @param string $inputs
   *   A data of curl request.
   *
   * @return array
   *   A respond data.
   */
  public function curlPost($resource, $inputs, $api) {
    return $this->executeCurl($resource, "POST", $inputs, $api);
  }

  public function searchBySiret($siret) {
    $resources = [
      "etablissements",
      $siret,
    ];
    return $this->curlGet($resources, [], self::API_URLv3);
  }

  public function searchBySirene($sirene) {
    $resources = [
      "unites_legales",
      $sirene,
    ];
    return $this->curlGet($resources, [], self::API_URLv3);
  }
  
  public function searchByName($name) {
    $resources = [
      "full_text",
      $name,
    ];
    return $this->curlGet($resources, [], self::API_URLv1);
  }
  
  /**
   * Function to return first element of the array, compatability with PHP 5, note that array_key_first is only available for PHP > 7.3.
   *
   * @param array $array
   *   Associative array.
   *
   * @return string
   *   The first key data.
   */
  public static function arrayKeyfirst($array){
    if (!function_exists('array_key_first')) {
        foreach($array as $key => $unused) {
            return $key;
        }
        return NULL;
    }else{
        return array_key_first($array);
    }
  }

}

Le contrôleur

Le contrôleur sert de callback pour l'appel ajax - 
EntrepriseAjaxController.php

<?php

namespace Drupal\bluedropfr_syndicalisation_new\Controller;

use Drupal\Core\Controller\ControllerBase;

use Drupal\Component\Serialization\JSON;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Drupal\bluedropfr_syndicalisation_new\EntrepriseDataApiManager;

class EntrepriseAjaxController extends ControllerBase {
  
  //function for autocomplete
  public function entreprise_data_autocomplete(Request $request){
    $return = []; //our variable to fill with data to return to autocomplete result
    
    $search = "sirene";
    $search_string = \Drupal::request()->request->get('siret_sirene');
    if(strlen($search_string) > 9) $search = "siret";
    
    if($search_string == "") {
      $search_string = \Drupal::request()->request->get('query_name');
      $search = "full_text";
    }
    //$type = "housenumber"; // can be housenumber, street, locality or municipality.

    $edataApi = new EntrepriseDataApiManager();
    switch($search){
      case 'sirene':
        $return = $edataApi->searchBySirene($search_string);
        break;
      case 'siret':
        $return = $edataApi->searchBySiret($search_string);
        break;
      case 'full_text':
        $return = $edataApi->searchByName($search_string);
        $return = $return['etablissement'];
        break;
    }
    //echo "etsttt <br>";
    //print_r($return);
    //exit;
    return new JsonResponse(json_encode($return), 200, [], true);
  }
  
}

Le composant javascript pour les interactions avec l'utilisateur

Un fichier javascript va gérer les interactions avec les utilisateurs  - Ce fichier est encore spécifique au projet concerné. Un prochain commit viendra très prochainement le faire évoluer.
Il reste dynamiser l'autocomplétion sur les champs d'un formulaire. En créant une page de configuration dans le backoffice, l'utilisateur pourra faire le matching entre les champs des formulaires sur son site et les données de l'API. En utlisant les "Drupal behaviors" nous passerons ces informations au composant javascript.

//--> search by Siret ou Siren
    $('#edit-siret-siren').autocomplete({
        source : function(requete, reponse){ // les deux arguments représentent les données nécessaires au plugin
        $.ajax({
                url : Drupal.url('cgt-syndicalisation/entreprise-data-autocomplete'), // on appelle le script JSON
                dataType : 'json', // on spécifie bien que le type de données est en JSON
                type: "POST",
                data : {
                    //variable envoyé avec la requête vers le serveur
                    siret_sirene : $('#edit-siret-siren').val(), // on donne la chaîne de caractère tapée dans le champ de recherche 
                },
                success : function(donnee){
                    //donnee est la variable reçu du serveur avec les résultats
                    reponse($.map(donnee, function(objet){
                        var label = "";
                        var value = "";
                        var nom = "";
                        var numero_voie = "";
                        var type_voie = "";
                        var libelle_voie = "";
                        var code_postal = "";
                        var libelle_commune = "";
                        if(typeof objet.unite_legale === 'object' && objet.unite_legale != null) {
                            //console.log("dans objet unite_legale");
                            label = objet.unite_legale.denomination + " (" + objet.code_postal + ")";
                            nom = objet.unite_legale.denomination;
                            value = objet.siret;
                            numero_voie = objet.numero_voie;
                            type_voie = objet.type_voie;
                            libelle_voie = objet.libelle_voie;
                            code_postal = objet.code_postal;
                            libelle_commune = objet.libelle_commune;
                        }else{
                            //console.log("else not in objet unite_legale");
                            label = objet.denomination + " (" + objet.etablissement_siege.code_postal + ")";
                            value = objet.siren;
                            nom = objet.denomination;
                            numero_voie = objet.etablissement_siege.numero_voie;
                            type_voie = objet.etablissement_siege.type_voie;
                            libelle_voie = objet.etablissement_siege.libelle_voie;
                            code_postal = objet.etablissement_siege.code_postal;
                            libelle_commune = objet.etablissement_siege.libelle_commune;
                        }
                        
                        // on retourne cette forme de suggestion avec ces datas
                        return {
                            'label': label,
                            'value': value,
                            'nom': nom,
                            'numero_voie': numero_voie,
                            'type_voie': type_voie,
                            'libelle_voie': libelle_voie, 
                            'code_postal': code_postal,
                            'libelle_commune': libelle_commune,
                        }; 
                    }));
                }
            });
        }
    });

    $('#edit-siret-siren').on( "autocompleteselect", function( event, ui ) {
        var nom = ui.item.nom;
        var numero_voie = ui.item.numero_voie;
        var type_voie = ui.item.type_voie;
        var libelle_voie = ui.item.libelle_voie;
        var code_postal = ui.item.code_postal;
        var libelle_commune = ui.item.libelle_commune;
        $('#edit-entreprise').val(nom);
        $('#edit-adresse-rue-entreprise').val(numero_voie + " " + type_voie + " " + libelle_voie);
        $('#edit-code-postal-entreprise').val(code_postal);
        $('#edit-ville-entreprise').val(libelle_commune);
    });

    //--> search by full_text : nom de l'entreprise
    $('#edit-entreprise').autocomplete({
        source : function(requete, reponse){ // les deux arguments représentent les données nécessaires au plugin
        $.ajax({
                url : Drupal.url('cgt-syndicalisation/entreprise-data-autocomplete'), // on appelle le script JSON
                dataType : 'json', // on spécifie bien que le type de données est en JSON
                type: "POST",
                data : {
                    //variable envoyé avec la requête vers le serveur
                    query_name : $('#edit-entreprise').val(), // on donne la chaîne de caractère tapée dans le champ de recherche 
                },
                success : function(donnee){
                    //donnee est la variable reçu du serveur avec les résultats
                    reponse($.map(donnee, function(objet){
                        var label = "";
                        var value = "";
                        var type_voie = "";
                        var numero_voie = "";
                        var libelle_voie = "";
                        var code_postal = "";
                        var libelle_commune = "";
                        
                        label = objet.nom_raison_sociale + " (" + objet.code_postal + ")";
                        value = objet.nom_raison_sociale;
                        numero_voie = objet.numero_voie;
                        type_voie = objet.type_voie;
                        libelle_voie = objet.libelle_voie;
                        code_postal = objet.code_postal;
                        libelle_commune = objet.libelle_commune;
                        
                        // on retourne cette forme de suggestion avec ces datas
                        return {
                            'label': label,
                            'value': value,
                            'type_voie': type_voie,
                            'numero_voie': numero_voie,
                            'libelle_voie': libelle_voie, 
                            'code_postal': code_postal,
                            'libelle_commune': libelle_commune,
                        }; 
                    }));
                }
            });
        }
    });

    $('#edit-entreprise').on( "autocompleteselect", function( event, ui ) {
        var type_voie = ui.item.type_voie;
        var numero_voie = ui.item.numero_voie;
        var libelle_voie = ui.item.libelle_voie;
        var code_postal = ui.item.code_postal;
        var libelle_commune = ui.item.libelle_commune;
        $('#edit-adresse-rue-entreprise').val(numero_voie + " " + type_voie + " " + libelle_voie);
        $('#edit-code-postal-entreprise').val(code_postal);
        $('#edit-ville-entreprise').val(libelle_commune);
    });
  });

Testez-le !
Module Drupal API sirene Open Data France

Grand merci à @elie pour cette contribution !!