Dans cet article, je vais exposer les différentes implémentations possibles pour consommer un service de conversion des Euros en devises étrangères : https://fixer.io/ .

Spécifications

Le résultat attendu doit être le suivant:

● Une application permettant de renseigner dans un champ de saisie une somme en
euros, et d'avoir la conversion dans une autre devise. Une liste déroulante permet de
sélectionner la devise dans laquelle la somme en euros doit être convertie.
● Le calcul doit être instantané : dès que le montant en euros change ou que la devise
sélectionnée change, le montant converti doit changer.

Voici à quoi l'interface doit ressembler:


Fixer.io euro converter interface


EUR

 

 

Vous aurez besoin de récupérer une clé d'API en vous inscrivant gratuitement sur https://fixer.io . Elle sera à insérer à la place de 'YOUR_FIXER_IO_API_KEY' dans le code.

La documentation du service en ligne sur leur site sera également très utile.

Notons que la fonction qui permet de calculer la conversion de devise est payante sur fixer.io, mais ce n'est pas bloquant, nous utiliserons le service de récupération des taux de change de chacune de devise et c'est notre api fera le calcul.

Implémentations et architectures

En Javascript

Il s'agit ici de l'implémentation la plus basique possible, cette solution ne nécessite pas de serveur en chargeant la page HTML depuis votre système de fichier et vous avez l'application déjà prête à fonctionner. 

Il y a 2 parties à écrire:

- une classe ou un objet écrit en javascript qui s'occupe d'interroger le service et de retourner les résultats, appelons le fixerIoEuroConverter

- une page html représentant les champs requis inter-agissant avec notre objet fixerIoEuroConverter .

La classe javascript fixerIoEuroConverter

Voici le code source de notre objet qui va consommer le service de fixer.io:

/* fixerIoEuroConverter 
*
*  Classe Javascript qui consomme le service fixer.io
*  pour convertir des euros dans la devise choisie
*
*  @author : Benoît DEUFFIC
*
*  pour le débogage et la formation on utilise toujours console.log() à chaque étape importante du programme
*/


/* fixerIoEuroConverter 
* Constructeur 
* Pas de paramètres en entrée
* fixe les variables de base nécessaires à la consommation du service 
*/

function fixerIoEuroConverter (params) {

  /* paramètre url de base du service */
  this.baseUrl = params['baseUrl'];

  /* clé d'api fournie à l inscription au service */
  this.apiKey = 'YOUR_FIXER_IO_API_KEY_HERE';

  /* points d entrée pour consommer les fonctions */
  this.listCurEndpoint = params['currencyEndpoint'];
  this.convertEndpoint = params['convertEndpoint'];

  /* message générique si le service est indisponible quelqu en soit la raison */
  this.unavailableMessage = 'This service is unavailable. Please try later.';
  this.targets = params.targets;

}

/* fixerIoEuroConverter.init
*  méthode pour initialiser la page avec la liste des devises
*  tel que spécifié ici : https://fixer.io/documentation#supportedsymbols  
*
*  en entrée: (string) identifiant du composant select qui doit recevoir les options
*  en sortie: si réponse ok : remplie les options de la liste avec les identifiants et noms des devises retournées par le service
*             si réponse ko : remplie l élément dédié à l'affichage des erreurs retournées par le services
*/

fixerIoEuroConverter.prototype.init = function ( )
{
  var serviceLocation = this.baseUrl + this.listCurEndpoint;

  /* ici on a aussi la possibilité de construire les paramètres d'URL avec $.params(), très utile lorsqu'y il y a beaucoup
  * de paramètres à gérer
  * http://api.jquery.com/jquery.param/ 
  */
  var urlWithParams = serviceLocation + '?access_key=' +  this.apiKey;

  console.log('Querying: ' + urlWithParams + '...');

  $.ajax({
    context: this,
    url: urlWithParams,
    type: 'GET',
    crossDomain: true,
    jsonp: false,
    dataType: 'json',
    success: function (data) {
      console.log(data);
      /* testons si le service a répondu 'success=true' tel que spécifié dans la doc du service 
      *  là c'est ok, on fait le traitement
      */
      if ( data.success === true ) {

        /* le service a répondu, récupérons nos résultats tel que spécifié dans la doc du service. */
        var results = data.symbols;

        /* ici on parcourt la collection d'obbets retournés par le service en récupérant le couple clé-valeur des devises
        *  et on remplie nos options sous la forme de texte html ( plus performant que de manipuler DOM )
        */

        for (let [key, value] of Object.entries(results)) {
          this.targets.list.append("<option value='" + key + "'>" + value + "</option>\n");
        }
      }

      /* si non, on affiche le message d'erreur qui est retourné par le service */
      else {

        displayError ( this.targets.error, data.error.info );

      }
    },

    /* dans le cas de fixer.io, le service retourne toujours un code http 200, l'erreur étant indiquée dans le contenu de la réponse
    *  par contre c'est utile si jamais le service est indisponible ( code http différent de 200) et on affiche une erreur générique
    *  "service indisponible" quelqu'en soit la raison
    */
    error: function (data) {
      console.log( data );

      displayError ( this.targets.error, this.unavailableMessage );

    }

  });
};

/* fixerIoEuroConverter.convert
*  méthode de conversion de la somme saisie en euros dans la devise choisie 
*  tel que spécifié ici : https://fixer.io/documentation#convertcurrency 
*  !! Ne fonctionne que si on a souscrit une license payante !!
*  --> sinon on va utiliser une autre méthode basée sur les derniers taux connus
*
*  en entrée: (string) amount : la somme saisie dans le champ texte
*             (string) currency :  l'identifiant de 3 lettres de la devise choisie dans la liste déroulante
*
*  en sortie: si amount est vide ou nul, ne fait rien
*             si ok, affiche le résultat de la conversion
*             si ko, affiche le message d'erreur du service et retourne faux
*/

fixerIoEuroConverter.prototype.convert = function ( amount, currency )
{
  /* ne faisons rien si la valeur amount est nulle ou vide */
  if ( !amount || 0 === amount.length ) { return ; }

  /* réinitialisons le champs d'erreur */
  $( "#error" ).hide();

  var serviceLocation = this.baseUrl + this.convertEndpoint;

  /* ici on a aussi la possibilité de construire les paramètres d'URL avec $.params(), très utile lorsqu'y il y a beaucoup
  * de paramètres à gérer
  * http://api.jquery.com/jquery.param/ 
  */
  var urlWithParams = serviceLocation + '?access_key=' +  this.apiKey
    + '&from=EUR'
    + '&to=' + currency
    + '&amount=' + amount;


  console.log('Querying: ' + urlWithParams + '...');

  $.ajax({
    context: this,
    url: urlWithParams,
    type: 'GET',
    crossDomain: true,
    jsonp: false,
    dataType: 'json',
    success: function (data) {
      console.log(data);
      if ( data.success === true ) {

        /* on récupère notre montant converti dans la devise choisie */
        var conversionAmount = data.result;
        /* on récupère la devise choisie dans la requête*/
        var choosedCurrency = data.query.to;

        displayResult(this.targets.success, conversionAmount + ' ' + choosedCurrency);
      }
      else {

        displayError ( this.targets.error, data.error.info );

      }
    },
    /* dans le cas de fixer.io, le service retourne toujours un code http 200, l'erreur étant indiquée dans le contenu de la réponse
    *  par contre c'est utile si jamais le service est indisponible ( code http différent de 200) et on affiche une erreur générique
    *  "service indisponible" quelqu'en soit la raison
    */
    error: function (data) {
      console.log( data );

      displayError ( this.targets.error, that.unavailableMessage );

    }
  });
};


/* displayError
*  fonction générique qui gère l'affichage des messages d'erreur retournés par le service
*  évite la répétition du même code un peu partout dans le programme.
*  en entrée : (string) message d'erreur
*  en sortie : remplie l'élément erreur
*/

function displayError ( target, message ) {

  target.empty();
  target.append( message );
  target.show();

};

/* displayResult
*  fonction générique qui gère l'affichage du résultat retourné par le service
*  évite la répétition du même code un peu partout dans le programme.
*  en entrée : (string) valeur du résultat
*  en sortie : remplie l'élément du résultat
*/

function displayResult (target, value ) {

  target.empty();
  target.append( value );


}

/* hideFields
*  fonction générique qui cache nos champs de résultats
*/

function hideFields(targets) {

  targets.success.empty();
  targets.error.hide();

}

A la suite de cette classe sont déclarées des fonctions utilitaires globale qui vont manipuler l'affichage de notre interface.

L'objet Javascript en détail
Constructeur

Déclarons le constructeur de notre objet de cette façon:

function fixerIoEuroConverter (params) {

  /* paramètre url de base du service */
  this.baseUrl = params['baseUrl'];

  /* clé d'api fournie à l inscription au service */
  this.apiKey = 'YOUR_FIXER_IO_API_KEY_HERE';

  /* points d entrée pour consommer les fonctions */
  this.listCurEndpoint = params['currencyEndpoint'];
  this.convertEndpoint = params['convertEndpoint'];

  /* message générique si le service est indisponible quelqu en soit la raison */
  this.unavailableMessage = 'This service is unavailable. Please try later.';
  this.targets = params.targets;

}

Il prend en entrée un tableau de paramètres qui comprend les paires clé-valeur suivantes et viendront alimenter les propriétés correspondantes de notre objet:

  • baseUrl: L'adresse web du service fixer.io,
  • listCurEnpoint: l'URI renvoie la liste des devises prises en charge,
  • convertEnpoint: comme dit plus haut on utiliserans pas la méthode de calcul de conversion de fixerio.io, mais la liste des taux de changes, on verra ensuite que notre objet se chargera de faire le calcul de la conversion dans la devise sélectionnée.
  • targets: les éléments HTML cibles à manipuler pour: la liste sélectionnable des devises,l'affichage des erreurs et des résultats.

La propriété apiKey reste en dur ici ainsi votre clé est offusquée de la page html qui appelera notre objet.

La propriété unavailableMessage contenant un emssage générique quelque soit l'erreur levée est codée en dur ici mais libre à vous de la rendre paramétrable en, rajouter une entrée correspondante à notre tableau de paramètres.

Méthodes

La première méthode que nous appellons init() va récupérer la liste des devises disponibles pour alimenter la liste de notre interface:

fixerIoEuroConverter.prototype.init = function ( )
{
  var serviceLocation = this.baseUrl + this.listCurEndpoint;

  /* ici on a aussi la possibilité de construire les paramètres d'URL avec $.params(), très utile lorsqu'y il y a beaucoup
  * de paramètres à gérer
  * http://api.jquery.com/jquery.param/ 
  */
  var urlWithParams = serviceLocation + '?access_key=' +  this.apiKey;

  console.log('Querying: ' + urlWithParams + '...');

  $.ajax({
    context: this,
    url: urlWithParams,
    type: 'GET',
    crossDomain: true,
    jsonp: false,
    dataType: 'json',
    success: function (data) {
      console.log(data);
      /* testons si le service a répondu 'success=true' tel que spécifié dans la doc du service 
      *  là c'est ok, on fait le traitement
      */
      if ( data.success === true ) {

        /* le service a répondu, récupérons nos résultats tel que spécifié dans la doc du service. */
        var results = data.symbols;

        /* ici on parcourt la collection d'obbets retournés par le service en récupérant le couple clé-valeur des devises
        *  et on remplie nos options sous la forme de texte html ( plus performant que de manipuler DOM )
        */

        for (let [key, value] of Object.entries(results)) {
          this.targets.list.append("<option value='" + key + "'>" + value + "</option>\n");
        }
      }

      /* si non, on affiche le message d'erreur qui est retourné par le service */
      else {

        displayError ( this.targets.error, data.error.info );

      }
    },

    /* dans le cas de fixer.io, le service retourne toujours un code http 200, l'erreur étant indiquée dans le contenu de la réponse
    *  par contre c'est utile si jamais le service est indisponible ( code http différent de 200) et on affiche une erreur générique
    *  "service indisponible" quelqu'en soit la raison
    */
    error: function (data) {
      console.log( data );

      displayError ( this.targets.error, this.unavailableMessage );

    }

  });
};

Avant Javascript ES6, la définition des méthodes est instanciée dans la propriété prototype native de Object, c'est l'héritage prototypal.

ECMAScript 2015 a apporté à javascript une nouvelle syntaxe pour déclarer des classes objet.

J'y reviendrai plus précisément dans un autre article mais notre objet se transforme très facilement dans le code suivant:

/* fixerIoEuroConverter 
*
*  Classe Javascript qui consomme le service fixer.io
*  pour convertir des euros dans la devise choisie
*
*  @author : Benoît DEUFFIC <benoit+oclock@deuffic.fr>
*
*  pour le débogage et la formation on utilise toujours console.log() à chaque étape importante du programme
*/


/* fixerIoEuroConverter 
* Constructeur 
* Pas de paramètres en entrée
* fixe les variables de base nécessaires à la consommation du service 
*/

class fixerIoEuroConverter {
   constructor (params) {

   /* paramètre url de base du service */
   this.baseUrl = params['baseUrl'];

  /* clé d'api fournie à l'inscription au service */
   this.apiKey = '412e09694588a610eee73b014122cb69';

   /* points d'entrée pour consommer les fonctions */
   this.listCurEndpoint = params['currencyEndpoint'];
   this.convertEndpoint = params['convertEndpoint'];

   /* message générique si le service est indisponible quelque'en soit la raison */
   this.unavailableMessage = 'This service is unavailable. Please try later.';
   this.targets = params.targets;
   }

/* fixerIoEuroConverter.init
*  méthode pour initialiser la page avec la liste des devises
*  tel que spécifié ici : https://fixer.io/documentation#supportedsymbols  
*
*  en entrée: (string) identifiant du composant select qui doit recevoir les options
*  en sortie: si réponse ok : remplie les options de la liste avec les identifiants et noms des devises retournées par le service
*             si réponse ko : remplie l'élément dédié à l'affichage des erreurs retournées par le services
*/

  init()
   {
        var serviceLocation = this.baseUrl + this.listCurEndpoint;

                                              /* ici on a aussi la possibilité de construire les paramètres d'URL avec $.params(), très utile lorsqu'y il y a beaucoup
                                              * de paramètres à gérer
                                              * http://api.jquery.com/jquery.param/ 
                                              */
        var urlWithParams = serviceLocation + '?access_key=' +  this.apiKey;

        console.log('Querying: ' + urlWithParams + '...');

        $.ajax({
            context: this,
            url: urlWithParams, 
            type: 'GET',
            crossDomain: true,  
            jsonp: false,
            dataType: 'json',
            success: function (data) {
               console.log(data);
               /* testons si le service a répondu 'success=true' tel que spécifié dans la doc du service 
               *  là c'est ok, on fait le traitement
               */
               if ( data.success === true ) {
 
                  /* le service a répondu, récupérons nos résultats tel que spécifié dans la doc du service. */
                  var results = data.symbols;

                  /* ici on parcourt la collection d'obbets retournés par le service en récupérant le couple clé-valeur des devises
                  *  et on remplie nos options sous la forme de texte html ( plus performant que de manipuler DOM )
                  */

                  for (let [key, value] of Object.entries(results)) {
                       this.targets.list.append("<option value='" + key + "'>" + value + "</option>\n");
                     }
               }

               /* si non, on affiche le message d'erreur qui est retourné par le service */
               else {

                    displayError ( this.targets.error, data.error.info );                  

                }
            },

            /* dans le cas de fixer.io, le service retourne toujours un code http 200, l'erreur étant indiquée dans le contenu de la réponse
            *  par contre c'est utile si jamais le service est indisponible ( code http différent de 200) et on affiche une erreur générique
            *  "service indisponible" quelqu'en soit la raison
            */  
            error: function (data) {
               console.log( data );

                    displayError ( this.targets.error, this.unavailableMessage );                  

            }

          });
   }

/* fixerIoEuroConverter.convert
*  méthode de conversion de la somme saisie en euros dans la devise choisie 
*  tel que spécifié ici : https://fixer.io/documentation#convertcurrency 
*  !! Ne fonctionne que si on a souscrit une license payante !!
*  --> sinon on va utiliser une autre méthode basée sur les derniers taux connus
*
*  en entrée: (string) amount : la somme saisie dans le champ texte
*             (string) currency :  l'identifiant de 3 lettres de la devise choisie dans la liste déroulante
*
*  en sortie: si amount est vide ou nul, ne fait rien
*             si ok, affiche le résultat de la conversion
*             si ko, affiche le message d'erreur du service et retourne faux
*/

  convert ( amount, currency )
   {
        /* ne faisons rien si la valeur amount est nulle ou vide */
        if ( !amount || 0 === amount.length ) { return ; }   

        /* réinitialisons le champs d'erreur */
        $( "#error" ).hide();

        var serviceLocation = this.baseUrl + this.convertEndpoint;

                                              /* ici on a aussi la possibilité de construire les paramètres d'URL avec $.params(), très utile lorsqu'y il y a beaucoup
                                              * de paramètres à gérer
                                              * http://api.jquery.com/jquery.param/ 
                                              */
        var urlWithParams = serviceLocation + '?access_key=' +  this.apiKey 
                                            + '&from=EUR'
                                            + '&to=' + currency
                                            + '&amount=' + amount;


        console.log('Querying: ' + urlWithParams + '...');

        $.ajax({
            context: this,
            url: urlWithParams, 
            type: 'GET',
            crossDomain: true, 
            jsonp: false,
            dataType: 'json',
            success: function (data) {
                console.log(data);
                if ( data.success === true ) {
 
                   /* on récupère notre montant converti dans la devise choisie */
                   var conversionAmount = data.result;
                   /* on récupère la devise choisie dans la requête*/
                   var choosedCurrency = data.query.to;

                   displayResult(this.targets.success, conversionAmount + ' ' + choosedCurrency);
                }
                else {

                    displayError ( this.targets.error, data.error.info );                  

                }
            },
            /* dans le cas de fixer.io, le service retourne toujours un code http 200, l'erreur étant indiquée dans le contenu de la réponse
            *  par contre c'est utile si jamais le service est indisponible ( code http différent de 200) et on affiche une erreur générique
            *  "service indisponible" quelqu'en soit la raison
            */
            error: function (data) {
               console.log( data );

                    displayError ( this.targets.error, that.unavailableMessage );

           }
          });
   }
}

Notez l'instruction context: this de notre instance Jquery AJAX: en Javascript dans des fonctions anonymes, une variable globale doit être dupliquée pour être exploitée dans le sous-bloc function. Il y a plusieurs méthodes de le faire:

  • utiliser la méthode bind() des functions:

    ma_function { mon_parametre.quelquechose ... }.bind('mon_paramètre, this)

    Elle n'est pas supportée par tous les navigateurs.

  • dupliquer en amont la variable this:

    var that = this;
  • Jquery.ajax admet en paramètres une clé context à laquelle passer la variable à appliquer.

Notre seconde méthode appelée convert est, vous en vous en doutez bien la plus importante; c'est elle qui répond à notre besoin. Celle-ci récupère donc la liste des taux de conversion par devises, cherche la devise demandée dans l'interface, applique le calcul et affiche le résultat:

fixerIoEuroConverter.prototype.convert = function ( amount, currency )
{
  /* ne faisons rien si la valeur amount est nulle ou vide */
  if ( !amount || 0 === amount.length ) { return ; }

  /* réinitialisons le champs d'erreur */
  $( "#error" ).hide();

  var serviceLocation = this.baseUrl + this.convertEndpoint;

  /* ici on a aussi la possibilité de construire les paramètres d'URL avec $.params(), très utile lorsqu'y il y a beaucoup
  * de paramètres à gérer
  * http://api.jquery.com/jquery.param/ 
  */
  var urlWithParams = serviceLocation + '?access_key=' +  this.apiKey
    + '&from=EUR'
    + '&to=' + currency
    + '&amount=' + amount;


  console.log('Querying: ' + urlWithParams + '...');

  $.ajax({
    context: this,
    url: urlWithParams,
    type: 'GET',
    crossDomain: true,
    jsonp: false,
    dataType: 'json',
    success: function (data) {
      console.log(data);
      if ( data.success === true ) {

        /* on récupère notre montant converti dans la devise choisie */
        var conversionAmount = data.result;
        /* on récupère la devise choisie dans la requête*/
        var choosedCurrency = data.query.to;

        displayResult(this.targets.success, conversionAmount + ' ' + choosedCurrency);
      }
      else {

        displayError ( this.targets.error, data.error.info );

      }
    },
    /* dans le cas de fixer.io, le service retourne toujours un code http 200, l'erreur étant indiquée dans le contenu de la réponse
    *  par contre c'est utile si jamais le service est indisponible ( code http différent de 200) et on affiche une erreur générique
    *  "service indisponible" quelqu'en soit la raison
    */
    error: function (data) {
      console.log( data );

      displayError ( this.targets.error, that.unavailableMessage );

    }
  });
};


Interface et page web

Nous utiliserons JQuery pour faire nous appels AJAX.

La page index.html dessine l'interface et à la fin de son chargement instancie notre objet Javascript pour initialiser la liste des devises en allant chercher le résultat de l'appel à l'API correspondant:

<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="js/fixerIoEuroConverter.js"></script>
    <meta charset="UTF-8">
  </head>
  <body>
     <h1> Fixer.io Euro converter </h1>
     <hr />
     <form>
       <input type='text' size='6' id='fldAmount'></input> EUR
       <select id ='currencies'>

       </select>
     </form>
    <span id='result' style="color:blue"></span>
    <br />
    <div id='error' style="color:red"></div>
 <script>
    var params = { baseUrl: 'http://data.fixer.io/api/',
                   currencyEndpoint: 'symbols',
                   convertEndpoint: 'latest',
                   targets: {error:  $( '#error' ), success:  $( '#result' ), list: $( '#currencies' ) }
                   }; 
   var api = new fixerIoEuroConverter(params);

      // déclencheur une fois la page html chargée
      $( document ).ready(function() {
         api.init(); 

      }.bind(api));
     // déclencheur au changement, coller, et touche clavier relâchée sur le champ de saisie du montant

     $( '#fldAmount' ).on("paste keyup change", function () {

         hideFields(params['targets']);
         api.convert($( '#fldAmount' ).val(), $( '#currencies' ).val());

     }.bind(api));

     /* déclencheur au changement de la sélection de la liste des devises.
      * ne fait rien si la valeur de la saisie n'est pas un nombre 
      */
     $( '#currencies' ).on("change", function () {

        if ( $.isNumeric( $( '#fldAmount' ).val() ) === false ) { return; } 

           api.convert($( '#fldAmount' ).val(), $( '#currencies' ).val());

     }.bind(api));

   </script>
  </body>
</html>

En revanche si vous avez besoin de rajouter des fonctionnalités, modifier ou maintenir le code il devient rapidement compliqué de redistribuer l'ensemble aux utilisateurs final et il faut alors penser à d'autres architectures un peu plus poussées comme les suivantes.

Il s'agira de gérer les appels à l'API de fixer.io dans du code exécuté du côté serveur et la récupération et l'affichage des résultat du coté navigateur.

En Javascript + Node.JS

Node.JS est un système de serveur de bas niveau programmable en Javascript.

Il est donc léger et rapide et peut être enrichi avec des frameworks .JS dont le plus connu est Express.

Il a l'avantage de pouvoir utiliser et implémenter très facilement les Websockets . Le client (navigateur) et le serveur peuvent communiquer sur la base d'événements déclenchables dans les deux sens. C'est très sympa pour écrire son propre Chat qui fera probablement l'objet d'un article plus tard.

Du côté serveur Node.JS

On va demander à notre serveur Node.js d'écouter sur un port réseau et d'attendre ou déclencher des événements.

En voici le code:

var http = require('http');
var httpClient = require('http');

var fs = require('fs');

// Chargement du fichier index.html affiché au client
var server = http.createServer(function (req, res) {
  fs.readFile('./index.html', 'utf-8', function (error, content) {
    res.writeHead(200, {"Content-Type": "text/html"});
    res.end(content);
  });
});

// Chargement de socket.io
var io = require('socket.io').listen(server);

console.log('Listening on port 8081...');

io.sockets.on('connect', function (socket) {
  console.log('Un client est connecté !');

  socket.on('init', function () {

    console.log('Initialisation...');
    /* paramètre url de base du service */
    var serviceHost = 'data.fixer.io';

    /* clé d'api fournie à l'inscription au service */
    var apiKey = 'YOUR_API_KEY_HERE';

    /* points d'entrée pour consommer les fonctions */
    var operation = 'symbols';

    /* message générique si le service est indisponible quelque'en soit la raison */
    var unavailableMessage = 'This service is unavailable. Please try later.';

    var endpoint = '/api/' + operation + '?access_key=' + apiKey;

    console.log('Querying: ' + serviceHost + endpoint + '...');

    var headers = {};
    var method = 'GET';

    var options = {
      host: serviceHost,
      path: endpoint,
      method: method,
      headers: headers
    };
    var that = this;
    var req = httpClient.get(options, function (res, socket) {
      res.setEncoding('utf-8');
      req.setTimeout(5000, function () {
        console.log('request timeout');
        that.emit('error', 'request timeout');
      });

      var responseString = '';

      res.on('data', function (data) {
        responseString += data;
      });

      res.on('end', function () {
        that.emit('message', responseString);
        console.log(responseString);
      });
    });

    req.end();

  });


  socket.on('convert', function (params) {
    var paramsObj = JSON.parse(params);
    console.log('Conversion de ' + paramsObj.amount + ' EUR. en ' + paramsObj.currency);

    /* paramètre url de base du service */
    var serviceHost = 'data.fixer.io';

    /* clé d'api fournie à l'inscription au service */
    var apiKey = 'YOUR_API_KEY_HERE';

    /* points d'entrée pour consommer les fonctions */
    var operation = 'latest';

    /* message générique si le service est indisponible quelque'en soit la raison */
    var unavailableMessage = 'This service is unavailable. Please try later.';

    var endpoint = '/api/' + operation + '?access_key=' + apiKey;

    console.log('Querying: ' + serviceHost + endpoint + '...');

    var headers = {};
    var method = 'GET';

    var options = {
      host: serviceHost,
      path: endpoint,
      method: method,
      headers: headers
    };


    var req = httpClient.get(options, function (res) {
      res.setEncoding('utf-8');
      req.setTimeout(5000, function () {
        console.log('request timeout');
        that.emit('error', 'request timeout');
      });

      var responseString = '';

      res.on('data', function (data) {
        responseString += data;
      });

      res.on('end', function () {
        console.log(responseString);
        var data = JSON.parse(responseString);
        var results = data.rates;
        var currency = paramsObj.currency;

        for (let [key, value] of Object.entries(results)) {
          if (key === currency) {
            var rateValue = value;
          }
        }
        /* A t'on bien trouvé le taux associé à la devise saisie ? si non, affiche une erreur et on sort. */
        if (!rateValue || 0 === rateValue.length) {
          that.emit('error', 'Selected currency rate was not found, can not apply conversion.');
          return;
        }

        /* on applique le taux au montant saisi */
        var conversionAmount = rateValue * paramsObj.amount;
        var result = conversionAmount + ' ' + currency;
        console.log('result is: ' + result);
        socket.emit('success', result);
      }.bind({'paramsObj': paramsObj, 'socket': socket}));
    }.bind({'socket': socket}));

    req.end();

  }.bind({'socket': socket}));
});

server.listen(8081);

Du côté du client: Javascript

Il s'agira d'écrire un code javascript utilisant l'objet natif socket pour qu'il se connecte à notre serveur Node.js et utilise des noms d'évenements websockets plutôt que des appels Ajax pour demander au serveur d'appeler l'API fixer.io et de traiter les résultats avant de les retourner pour les présenter.

Voici le code:

        var socket = io.connect('http://localhost:8081');

        socket.on('error', function (message) {
          $( '#error' ).append(message);
          $( '#error' ).show();
        });

        socket.on('success', function (message) {
          $( '#result' ).append(message);
        });

function reinitFields() {
$( '#error').hide();
$( '#result' ).empty();
} 

// déclencheur une fois la page html chargée
      $( document ).ready(function() {
             socket.emit('init');
             socket.on('message', function(message) {
               /* le service a répondu, récupérons nos résultats tel que spécifié dans la doc du service. */
                  var data = JSON.parse(message);
                  var results = data.symbols;
                  
                  /* ici on parcourt la collection d'obbets retournés par le service en récupérant le couple clé-valeur des devises
                  *  et on remplie nos options sous la forme de texte html ( plus performant que de manipuler DOM )
                  */

                  for (let [key, value] of Object.entries(results)) {
                       $( '#currencies' ).append("<option value='" + key + "'>" + value + "</option>\n");
                     }
          });
     });

     $( '#fldAmount' ).on("paste keyup change", function () {
        reinitFields();
        if ( $.isNumeric( $( '#fldAmount' ).val() ) === false ) { return; } 

        console.log('emit convert event');
        var params = { currency: $( '#currencies' ).val(), amount: $( '#fldAmount' ).val() };
        socket.emit('convert', JSON.stringify(params));

     });

     $( '#currencies' ).on("change", function () {
        reinitFields();
        if ( $.isNumeric( $( '#fldAmount' ).val() ) === false ) { return; } 

        console.log('emit convert event');
        var params = { currency: $( '#currencies' ).val(), amount: $( '#fldAmount' ).val() };
        socket.emit('convert',  JSON.stringify(params));
       
     });

La page html reste la même qu'au chapitre 2.1 .

En Javascript + Symfony

Dans Symfony il est très simple d'implémenter l'api qui va interroger fixerio.

Symfony: Installation et implémentation du projet

Créons notre nouveau projet sous Symfony avec composer:

 composer create-project symfony/skeleton sfFixerIo

Au préalable nous avons besoin des composants symfony/http-client et symfony/http-client-contracts et on peut s'assurer qu'ils sont bien installés dans le projet avec composer:

composer require symfony/http-client symfony/http-client-contracts

Comme le nom du paquet l'indique, il s'agit du client HTTP qui va nous servir à interroger fixer.io.

Création du contrôleur FixerIoController

Créons notre controller FixerIoController par la commande:

php bin/console generate:controller --no-interaction --controller=FixerIoController

Il sera le point d'entrée de notre API.

Implémentation du service fixerIoConverterService

Nous avons besoin d'un service que nous appellerons par exemple fixerIoConverterService et pour l'implémenter définissons d'abord son interface, en créant les fichiers dans un nouveau répertoire Service sous src/ :

Fichier fixerIoConverterServiceInterface.php:

<?php


namespace App\Service;


use Symfony\Contracts\HttpClient\HttpClientInterface;

interface fixerIoConverterServiceInterface
{

    public const BASE_URL = 'http://data.fixer.io/api/';
    public const API_KEY = 'YOUR_API_KEY_HERE';

    public const   SYMBOLS_ENDPOINT = 'symbols';
    public const   LATEST_ENDPOINT = 'latest';

    public function getSymbols(): string;

    public function calcConvert(string $currency, float $amount): float;

}

Cette interface définit les constantes de base pour apeller l'api fixer.io et deux méthodes getSymbols() qui récupére la liste des devises et calcConvert() qui va opérer le calcul de la conversion à partir du taux de la devise de notre choix retourné par fixer.io.

La classe du service fixerIoConverterService implémente notre interface:

<?php

namespace App\Service;

use Symfony\Contracts\HttpClient\HttpClientInterface;

class fixerIoConverterService implements fixerIoConverterServiceInterface
{

    private $client;

    public function __construct(HttpClientInterface $client)
    {
        $this->client = $client;
    }

    public function getSymbols(): string
    {

        $serviceLocation = self::BASE_URL . self::SYMBOLS_ENDPOINT . '?access_key=' . self::API_KEY;
        return $this->getResponse($serviceLocation)->getContent();
    }

    public function calcConvert(string $currency, float $amount): float
    {

        $serviceLocation = self::BASE_URL . self::LATEST_ENDPOINT . '?access_key=' . self::API_KEY;
        $res = $this->getResponse($serviceLocation)->toArray();
        $rates = $res['rates'];
        $convert = floatval(0);

        if (true === $res['success'] && array_key_exists($currency, $rates)) {
            $rate = $rates[$currency];
            $convert = $amount * $rate;
            return $convert;
        } else {
            return $convert;
        }

    }

    private function getResponse($url)
    {
        $res = null;
        $res = $this->client->request('GET', $url);
        return $res;
    }

}

Décorticons notre service:

  • Constructeur

    class fixerIoConverterService implements fixerIoConverterServiceInterface
    {
    
        private $client;
    
        public function __construct(HttpClientInterface $client)
        {
            $this->client = $client;
        }
    

    Nous injectons la dépendance du client HTTP dont le Service a besoin

  • Méthodes

      public function getSymbols(): string
        {
    
            $serviceLocation = self::BASE_URL . self::SYMBOLS_ENDPOINT . '?access_key=' . self::API_KEY;
            return $this->getResponse($serviceLocation)->getContent();
        }
    
        public function calcConvert(string $currency, float $amount): float
        {
    
            $serviceLocation = self::BASE_URL . self::LATEST_ENDPOINT . '?access_key=' . self::API_KEY;
            $res = $this->getResponse($serviceLocation)->toArray();
            $rates = $res['rates'];
            $convert = floatval(0);
    
            if (true === $res['success'] && array_key_exists($currency, $rates)) {
                $rate = $rates[$currency];
                $convert = $amount * $rate;
                return $convert;
            } else {
                return $convert;
            }
    
        }
    
        private function getResponse($url)
        {
            $res = null;
            $res = $this->client->request('GET', $url);
            return $res;
        }
    

    La méthode privée getResponse($url) sert à interroger fixer.io à partir de l'url du endpoint de la méthode demandée (symbols ou latest). La méthode getSymbols(), vous l'aurez compris récupère les symboles et retourne la chaîne au format json qui est fournie par fixer.io. La méthode calcConvert($currency, $amount) récupère les derniers taux connus, cherche celui de la devise demandée et applique le calcul en retournant un float.

Il nous reste à dire à notre contrôleur de consommer notre service en lui attribuant des points d'entrées dans le routing pour chacune des méthodes getSymbols et calcConvert:

<?php

namespace App\Controller;

use App\Service\fixerIoConverterServiceInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;


class FixerIoController extends AbstractController
{
    /**
     * @Route("/", methods={"GET","HEAD"}, name="accueil")
     */
    public function getHome()
    {
        return $this->render("index.html.twig");
    }

    /**
     * @Route("/api/currency/symbols", methods={"GET","HEAD"}, name="get_symbols")
     */

    public function getSymbols(fixerIoConverterServiceInterface $ioConverter)
    {
        return new Response($ioConverter->getSymbols(), 200);
    }


    /**
     * @Route("/api/currency/convert/{currency}/{amount}", methods={"GET","HEAD"}, name="get_convert")
     */

    public function getConvert(fixerIoConverterServiceInterface $ioConverter, string $currency, string $amount)
    {

        $value = floatval($amount);
        $convert = $ioConverter->calcConvert($currency, $value);
        $return = array('query'=>array('to'=>$currency), 'result'=> $convert );

        return new Response(json_encode($return));
    }
}

Injectons dans nos méthodes notre interface fixerIOConverterServiceInterface et appelons les méthodes de notre service correspondant à l'action, et voilà !

Avec Symfony de base nous savons faire une api cependant notez dans la méthode getConvert qu'il y a besoin de penser à encoder notre tableau return en json.

Et on peut faire mieux encore ! Il existe des bundles dont les plus connus sont api-platform (à mon avis complet et adapté pour de gros projets d'API) et FOSRestBundle qui fait ses preuves depuis une dizaine d'années maintenant. Nous creuserons un peu plus loin dans un prochain article.

Partie Javascript

Il ne nous reste plus qu'à adapter les paramètres de notre objet javascript pour qu'il interroge notre API symfony et faire une petite modification de l'URI de la méthode convert:

  • Index.html:

       var params = { baseUrl: 'http://localhost:8000/api/',
                       currencyEndpoint: 'symbols',
                       convertEndpoint: 'latest',
                       targets: {error:  $( '#error' ), success:  $( '#result' ), list: $( '#currencies' ) }
                       }; 
       var api = new fixerIoEuroConverter(params);

    Objet javascript:

     convert ( amount, currency )
       {
            /* ne faisons rien si la valeur amount est nulle ou vide */
            if ( !amount || 0 === amount.length ) { return ; }   
    
            /* réinitialisons le champs d'erreur */
            $( "#error" ).hide();
    
            var serviceLocation = this.baseUrl + this.convertEndpoint;
    
                                                  /* ici on a aussi la possibilité de construire les paramètres d'URL avec $.params(), très utile lorsqu'y il y a beaucoup
                                                  * de paramètres à gérer
                                                  * http://api.jquery.com/jquery.param/ 
                                                  */
            var urlWithParams = serviceLocation + '/' + currency +'/' + amount;
    ...
    }