Module Drupal pour utiliser l'API de géolocalisation geo.gouv.fr dans un formulaire (à la place du geocoding de google :)

Jeudi 15 Juillet 2021

Voici le code d'un second module en cours de contribution sur drupal.org. Avant de poursuivre, nous libérons le code et la démarche. Il permet de s'affranchir de l'API de geocoding de Google pour les adresses en France.

Image illustrant l'utilisation de l'API geocoding de la plateforme Open Data en France

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. Le projet est ici de simplifier le renseignement de leur adresse postale.

Les utilisateurs disposent ici d'un champ d'autocompletion lors de la saisie de leur adresse. Cette fonctionnalité, souvent relayée par les données fournies par les services (payants) de geocoding de google, est ici fournie par l'API de la Plateforme ouverte des données publiques françaises. En renseignant le champ adresse, le module vient suggérer des adresses (numéro de la voie + libellé de la voie), puis complète les champs complémentaires d'adresse (code postale, ville, etc.)

L'objectif du module

Le module est disponible ici : 
https://www.drupal.org/project/api_adresses_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 adresses disponibles sur le territoire français. 

L'API adresse

https://geo.api.gouv.fr/adresse
Dans le cadre du Service Public de la Donnée, de nombreux jeux de données sont ouverts. La base de données ADRESSE est consultable librement et gratuitement !

Ce service interroge l’API adresse 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 d'un champ adresse :

Création de la route

Création de la route pour l'Ajax callback dans le fichier .routing.yml du module :

bluedropfr_syndicalisation_new.address_autocomplete:
  path: '/cgt-syndicalisation/address-autocomplete'
  defaults:
    _controller: '\Drupal\bluedropfr_syndicalisation_new\Controller\AddressAjaxController::address_autocomplete'
    _title: 'Autocomplete'
    _format: json
  requirements:
    _access: 'TRUE'

Le fichier manager

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

<?php

namespace Drupal\bluedropfr_syndicalisation_new;

use Drupal\Component\Serialization\Json;
use GuzzleHttp\Exception\RequestException;
/**
 * Basic manager of module.
 */
class GeoAPIGouvFrManager {
  /**
   * API request url.
   */
  const API_URL = 'https://api-adresse.data.gouv.fr';
 
  public $client;
  /**
   * Constructor.
   */
  public function __construct() {
    if (!function_exists('curl_init')) {
      $msg = 'Geo API gouv.fr 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) {
    if (!function_exists('curl_init')) {
      $msg = 'Geo API gouv.fr requires CURL module';
      \Drupal::logger('bluedropfr_syndicalisation_new')->error($msg);
      return NULL;
    }
    $api_url = self::API_URL . "/" . $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) {
    return $this->executeCurl($resource, "GET", $inputs);
  }

  /**
   * 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) {
    return $this->executeCurl($resource, "POST", $inputs);
  }

  /**
   * Search place by street number, city name, street name...
   *
   * @param array $options
   *   An array of search options.
   *
   * @return array
   *   An array of search results.
   */
  public function searchPlace($options) {
    return $this->curlGet("search", $options);
  }

  /**
   * Get campaigns by type.
   *
   * @param string $type
   *   A campaign type.
   *
   * @return array
   *   An array of options.
   */
  public function buildOptionsSearchAutocomplete($searchString, $type, $limit = 5, $autocomplete = 1) {
    $options = [
        "q" => $searchString,
        "type" => $type,
        "autocomplete" => $autocomplete,
        "limit" => $limit,
    ];
    return $options;
  }
  
  /**
   * 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

Un contrôleur sert de callback pour l'appel Ajax.
AddressAjaxController.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\ebiz_syndicalisation_new\GeoAPIGouvFrManager;

class AddressAjaxController extends ControllerBase {
  
  //function for autocomplete
  public function address_autocomplete(Request $request){
    $return = []; //our variable to fill with data to return to autocomplete result
    
    $search_string = \Drupal::request()->request->get('name_startsWith');
    $type = "housenumber"; // can be housenumber, street, locality or municipality.

    $geoApi = new GeoAPIGouvFrManager();
    $return = $geoApi->searchPlace($geoApi->buildOptionsSearchAutocomplete($search_string, $type, 10));

    return new JsonResponse(json_encode($return['features']), 200, [], true);
  }
  
}

Le composant javascript pour les interactions avec l'utilisateur

(function ($) {
  $(document).ready(function() {
    $('#edit-adresse-rue').autocomplete({
			source : function(requete, reponse){ // les deux arguments représentent les données nécessaires au plugin
			$.ajax({
			        url : Drupal.url('cgt-syndicalisation/address-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
					name_startsWith : $('#edit-adresse-rue').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){
							return {'label':objet.properties.label,
'value':objet.properties.name,
'postcode':objet.properties.postcode,
'id':objet.properties.id, 
'city':objet.properties.city,}; // on retourne cette forme de suggestion
						}));
					}
				});
			}
		});

	$('#edit-adresse-rue').on( "autocompleteselect", function( event, ui ) {
		var postcode = ui.item.postcode;
		var city = ui.item.city;
		$('#edit-code-postal').val(postcode);
		$('#edit-ville').val(city);
	});

  });
})(jQuery);

Pour plus d'informations sur les attributs JSON de l'API : https://github.com/geocoders/geocodejson-spec/tree/master/draft

Testez-le !
Module Drupal API adresses Open Data France

Grand merci à @elie pour cette contribution !!