Api-web ( 10 / 12 articles - Voir la liste )

Erreur Google Map vide

Si vous intégrez une Google Map mais qu'elle reste vide, vérifiez bien :

  • que les scripts javascript Google sont bien chargés
  • que l'initialisation de la google Map est correcte, sans oublier les paramètres center et zoom.

Voici l'exemple fourni par Google dans la documentation.

Astuce Centrer automatiquement une carte en fonction des points affichés

Lorsque vous affichez plusieurs points (= marqueurs) sur une Google Map, vous voulez en général que la carte soit suffisamment dézoomée pour permettre de les afficher tous.

Carte par défaut

Pour afficher une carte avec plusieurs marqueurs, vous pouvez suivre le deuxième exemple de cet article.

Pour rappel, le code javascript utilisé est celui-ci :

function initMap() {

    // Création de la carte
    var paris_latlong = {lat: 48.866667, lng: 2.333333};
    var map = new google.maps.Map(document.getElementById('map'), {
      zoom: 10,
      center: paris_latlong
    });

    // Ajout de marqueurs
    var markerList = [
        { lat: 48.837347, lng: 2.291787, title: "Marqueur n°1" },
        { lat: 48.879681, lng: 2.379958, title: "Marqueur n°2" },
        { lat: 48.822399, lng: 2.498793, title: "Marqueur n°3" }
    ];

    for (var i = 0, length = markerList.length; i < length; i++) {
        var latLng = new google.maps.LatLng(markerList[i].lat, markerList[i].lng);
        var marker = new google.maps.Marker({
            position: latLng,
            map: map,
            title: markerList[i].title
        });
    }
}

Par défaut, le centre et le niveau de zoom sont définis lors de l'initialisation de la carte. Il serait plus intéressant que ces valeurs soit calculées automatiquement.

Limiter la carte

Pour cela Google Map permet de définir les limites de la carte : les bounds.

Le code est alors légèrement modifié :

function initMap() {

    // Création de la carte
    var paris_latlong = {lat: 48.866667, lng: 2.333333};
    var map = new google.maps.Map(document.getElementById('map'), {
      zoom: 10,
      center: paris_latlong
    });

    // Initialisation des limites de la carte
    var bounds = new google.maps.LatLngBounds();

    // Ajout de marqueurs
    var markerList = [
        { lat: 48.837347, lng: 2.291787, title: "Marqueur n°1" },
        { lat: 48.879681, lng: 2.379958, title: "Marqueur n°2" },
        { lat: 48.822399, lng: 2.498793, title: "Marqueur n°3" }
    ];

    for (var i = 0, length = markerList.length; i < length; i++) {
        var latLng = new google.maps.LatLng(markerList[i].lat, markerList[i].lng);
        var marker = new google.maps.Marker({
            position: latLng,
            map: map,
            title: markerList[i].title
        });

        // Ajout des coordonnées du marqueur aux limites
        bounds.extend(markerList[i].lat, markerList[i].lng);
    }

    // Application des limites à la carte
    map.fitBounds(bounds);
}

Explications :

  • On instancie un objet limites (= LatLngBounds) et on lui transmet les coordonnées de tous les marqueurs affichés.
  • On demande ensuite à la carte de respecter ces limites.

On obtient alors ce résultat :

Centrage de la carte avec 3 points

Si on enlève le 3ème marqueur, la carte est automatiquement recentrée :

Centrage de la carte avec 2 points

Inconvénient

Le problème de cette technique, c'est que si vous n'affichez qu'un seul point, le zoom sera très important :

Centrage de la carte avec 1 point

Zoom maximal fixe

La solution la plus simple est d'ajouter un zoom maximal à la carte, lors de son initialisation :

var map = new google.maps.Map(document.getElementById('map'), {
    zoom: 10,
    center: paris_latlong,
    maxZoom: 11
});

Inconvénient

Le gros problème, c'est que l'utilisateur sera bloqué et ne pourra pas zoomer plus que la valeur que vous aurez définie.

Zoom calculé

À la place d'utiliser le zoom maximal, on peut modifier le zoom automatiquement en fonction des limites de la carte :

var zoomChangeBoundsListener = google.maps.event.addListener(map, 'bounds_changed', function(event) {
    google.maps.event.removeListener(zoomChangeBoundsListener);
    map.setZoom(Math.min( 11, map.getZoom()));
});

Explications :

  • On ajoute un écouteur sur l'évènement bounds_changed fourni par Google Map.
  • L'écouteur est supprimé afin de laisser l'utilisateur zoomer/dézoomer à sa guise.
  • Quand les limites de la carte sont modifiées, le zoom est à nouveau calculé pour ne pas être supérieur à la valeur souhaitée.

Remarque :

Il faut ajouter ce code après l'initialisation de la carte, mais avant d'appeler la méthode fitBounds(). Sans quoi l'écouteur ne captera pas l'évènement.

Astuce Utiliser l'API Google Map

Voici une liste d'utilisations courantes de l'API Google Map :

  • Créer une carte
  • Afficher des points (= marqueurs)
  • Afficher un point avec un marqueur personnalisé
  • Afficher une bulle au clic sur un marqueur (= InfoWindow)

Google Key API

Pour pouvoir utiliser l'API Google, vous devez tout d'abord avoir un compte Google et générer une clé.

  • Rendez-vous à cette adresse et connectez-vous à votre compte Google
  • Créez un nouveau projet en cliquant sur Continuer
  • Renseignez les noms de domaines qui auront le droit d'utiliser la clé (ex: *.mon-site.com, 127.0.0.1, localhost, ...)
  • Copiez la clé de l'API :

    Copier la clé d'API Google Map

Une carte basique

Google Map basique

Voici le code HTML/Javascript pour afficher une carte simple, sans marqueur :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <style>
       #map {
        height: 400px;
        width: 600px;
       }
    </style>
  </head>
  <body>
    <h3>My Google Maps Demo</h3>
    <div id="map"></div>
    <script>
    function initMap() {
        var paris_latlong = {lat: 48.866667, lng: 2.333333};
        var map = new google.maps.Map(document.getElementById('map'), {
            zoom: 10,
            center: paris_latlong
        });
    }
    </script>
    <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"></script>
  </body>
</html>

Explications :

  • Le code HTML ne contient qu'un élément div, dans lequel la carte sera générée en javascript. Cet élément doit avoir une largeur et une hauteur (cf. CSS).
  • On inclue la lib javascript de Google, en lui passant la clé générée précédemment.
  • Le code générant la carte ne sera exécuté qu'une fois toute la page chargée
  • Une fois le script chargé, la fonction initMap() est appelée en callback.
  • Pour initialiser la carte, il faut fournir au moins deux paramètres : les coordonnées du point central de la carte (latitude/longitude), et le niveau du zoom à appliquer.

Remarque :

Comme présenté dans la documentation officielle, Google conseille d'indiquer la clé d'API lors de l'appel à la librairie javascript. Il est toutefois possible de s'en passer (en phase de test par exemple), en modifiant l'URL :

<script async defer src="https://maps.googleapis.com/maps/api/js?callback=initMap"></script>

Afficher des marqueurs

Afficher des marqueurs

Si on veut afficher des marqueurs, le code javascript devient :

function initMap() {

    // Création de la carte
    var paris_latlong = {lat: 48.866667, lng: 2.333333};
    var map = new google.maps.Map(document.getElementById('map'), {
      zoom: 10,
      center: paris_latlong
    });

    // Ajout de marqueurs
    var markerList = [
        { lat: 48.837347, lng: 2.291787, title: "Marqueur n°1" },
        { lat: 48.879681, lng: 2.379958, title: "Marqueur n°2" },
        { lat: 48.822399, lng: 2.498793, title: "Marqueur n°3" }
    ];

    for (var i = 0, length = markerList.length; i < length; i++) {
        var latLng = new google.maps.LatLng(markerList[i].lat, markerList[i].lng);
        var marker = new google.maps.Marker({
            position: latLng,
            map: map,
            title: markerList[i].title
        });
    }
}

Explications :

  • On initialise un tableau contenant la liste des marqueurs, avec leurs coordonnées (latitude/longitude) et un libellé à afficher au survol (title).
  • On parcourt ce tableau et on instancie des marqueurs, rattachés à la carte créée précédemment.

Remarque :

Pour centrer automatiquement la carte sur ces points, vous pouvez suivre cet article.

Afficher un marqueur personnalisé

Il est agréable d'utiliser ses propres icônes de marqueur.

Afficher un marqueur personnalisé

Pour cela, il n'y a quasiment rien à ajouter :

function initMap() {

    // Création de la carte
    var paris_latlong = {lat: 48.866667, lng: 2.333333};
    var map = new google.maps.Map(document.getElementById('map'), {
      zoom: 10,
      center: paris_latlong
    });

    // Ajout du marqueur
    var marker = new google.maps.Marker({
        position: paris_latlong,
        map: map,
        title: 'Paris',
        icon: 'http://www.devnotebook.fr/public/articles/api-diverses/utiliser-l-api-google-map-06.png'
    });
}

Explication :

On a juste ajouté l'attribut icon au marqueur.

Afficher une bulle d'information

Afficher des marqueurs

Pour afficher une bulle d'information (InfoWindow) au clic sur le marqueur, voici ce que devient le code javascript :

function initMap() {

    // Création de la carte
    var paris_latlong = {lat: 48.866667, lng: 2.333333};
    var map = new google.maps.Map(document.getElementById('map'), {
      zoom: 10,
      center: paris_latlong
    });

    // Ajout du marqueur
    var marker = new google.maps.Marker({
        position: paris_latlong,
        map: map,
        title: 'Paris'
    });

    // Ajout d'une InfoWindow
    var htmlContent = '<div class="info-window-container">';
        htmlContent += ' <h2>Mon marqueur</h2>';
        htmlContent += ' <p>Une petite description</p>';
        htmlContent += ' <p><a href="https://developers.google.com/maps/documentation/javascript/infowindows">Lien vers l\'API Google</a></p>';
        htmlContent += '</div>';
    var infoWindow = new google.maps.InfoWindow({
        content: htmlContent
    });

    marker.addListener('click', function() {
        infoWindow.open(map, marker);
    });
}

Explications :

  • On stocke du HTML dans une variable javascript.
  • On instancie un objet InfoWindow avec pour contenu ce code HTML.
  • On ajoute un écouteur pour l'évènement clic sur le marqueur, pour que l'InfoWindow s'ouvre.

Astuce Géocoder une adresse et récupérer les limites d'une ville

Voici une classe utilitaire pour géocoder une adresse et/ou récupérer les limites (= bounds) d'une ville. Elle utilise l'API Google Map.

/**
 * Classe utilitaire de géocodage.
 *
 * Permet de :
 *  - géocoder simplement une adresse auprès de l'API Google Map.
 *  - récupérer les limites d'une ville.
 *
 * L'appel à l'API Google Map utilise curl.
 *
 * @class Geocoder
 */
class Geocoder {

  /**
   * L'URL d'appel au service de geocodage de Google.
   *
   * @const string
   */
  const BASE_URL = 'https://maps.google.com/maps/api/geocode/json';

  /**
   * Clé d'API Google Map.
   *
   * @var string
   */
  private $apiKey;

  /**
   * ID client à utiliser avec la clé privée ci-dessous.
   *
   * @var string
   */
  private $clientID;

  /**
   * Clé privée permettant de dépasser la limitation de 2000 requêtes/jour.
   *
   * @var string
   */
  private $privateKey;

  /**
   * Copnstructeur.
   *
   * @param string $api_key
   *   Clé d'API Google Map.
   * @param string $client_id
   *   ID client pour l'API. Laisser vide pour une utilisation classique.
   * @param string $private_key
   *   Clé secrète associéeà 'ID client. Laisser vide pour
   *   une utilisation classique.
   */
  public function __construct($api_key, $client_id = '', $private_key = '') {
    $this->apiKey = $api_key;
    $this->clientID = $client_id;
    $this->privateKey = $private_key;
  }

  /**
   * Retourne le nom d'une ville à partir du tableau de résultat de géocodage.
   *
   * @param array $geocode_result
   *   Tableau de résultat d'un géocodage.
   *
   * @return string
   *   Un tableau dont les clés sont 'top', 'right, 'bottom' et 'left'
   */
  public function getName(array $geocode_result) {

    $city_name = '';
    $address_components = $geocode_result['address_components'];

    $i = 0;
    $length = count($address_components);

    while (empty($city_name) && $i < $length) {

      if (!empty($address_components[$i]['types'][0]) && $address_components[$i]['types'][0] == 'locality') {
        $city_name = $geocode_result['address_components'][$i]['long_name'];
      }

      $i++;
    }

    return $city_name;
  }

  /**
   * Retourne les limites d'une ville.
   *
   * @param array $geocode_result
   *   Tableau de résultat d'un géocodage.
   *
   * @return array
   *   Un tableau dont les clés sont 'top', 'right, 'bottom' et 'left'
   */
  public function getBounds(array $geocode_result) {

    $bounds = [];

    if (!empty($geocode_result['geometry']['bounds'])) {

      $geo_bounds = $geocode_result['geometry']['bounds'];

      $bounds = [
        'top' => $geo_bounds['northeast']['lat'],
        'right' => $geo_bounds['northeast']['lng'],
        'bottom' => $geo_bounds['southwest']['lat'],
        'left' => $geo_bounds['southwest']['lng'],
      ];
    }
    return $bounds;
  }

  /**
   * Retourne le nom de la ville et ses limites.
   *
   * @param string $city_name
   *   Nom de la ville, avec son code postal.
   *
   * @return array
   *   Un tableau dont les clés sont 'name' et 'bounds'
   */
  public function getCity($city_name) {

    $city = NULL;

    $geocode = $this->geocode($city_name);

    if (!empty($geocode)) {

      $city['name'] = $this->getName($geocode);
      $city['bounds'] = $this->getBounds($geocode);
    }

    return $city;
  }

  /**
   * Retourne le résultat d'un géocodage sur l'adresse en argument.
   *
   * @param string $address
   *   Adresse à géocoder.
   *
   * @return array
   *   Le flux json de Google décodé
   */
  public function geocode($address) {

    $result = NULL;

    $query_parameters = [
      'address' => $address,
      'key' => $this->apiKey,
      'result_type' => 'locality',
      'components' => 'country:FR',
      'sensor' => 'false',
    ];

    $url = self::BASE_URL;
    $query_string = '';

    foreach ($query_parameters as $key => $value) {
      $query_string .= '&' . $key . '=' . urlencode($value);
    }
    $url .= '?' . substr($query_string, 1);

    if (!empty($this->clientID) && !empty($this->private_key)) {
      $url = $this->signUrl($url);
    }

    $json_response = $this->curlFileGetContent($url);
    $response = json_decode($json_response, TRUE);

    if ($response['status'] == 'OK') {
      $result = $response['results'][0];
    }

    return $result;
  }

  /**
   * Retourne l'URL en argument en y ajoutant le paramètre de signature.
   *
   * Ce paramètre de signature est construit à partir de la clé privée et de
   * l'ID client. Elle permet notamment de dépasser les 2000 requêtes/jour.
   *
   * @param string $unsigned_url
   *   URL à signer.
   *
   * @return string
   *   L'URL signée
   */
  private function signUrl($unsigned_url) {

    $url = parse_url($unsigned_url);

    $url_part_to_sign = $url['path'] . "?" . $url['query'];

    if (strpos($url_part_to_sign, 'client') === FALSE) {
      $url_part_to_sign .= '&client=' . $this->clientID;
      $unsigned_url .= '&client=' . $this->clientID;
    }

    // Décode la clé privée dans son format binaire
    $decoded_key = $this->decodeBase64UrlSafe($this->privateKey);

    // Crée une signature binaire via HMAC SHA1
    $signature = hash_hmac('sha1', $url_part_to_sign, $decoded_key, TRUE);
    $encoded_signature = $this->encodeBase64UrlSafe($signature);

    return $unsigned_url . '&signature=' . $encoded_signature;
  }

  /**
   * Appelle l'URL en argument via curl, et retourne le résultat de l'exécution.
   *
   * @param string $url
   *   URL à appeler.
   *
   * @return mixed
   *   Le retour de l'API
   */
  private function curlFileGetContent($url) {

    $c = curl_init();

    curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($c, CURLOPT_URL, $url);

    $content = curl_exec($c);
    curl_close($c);

    return $content;
  }

  /**
   * Encode une chaîne en base 64.
   *
   * Échappe les caractères gênants dans une URL (+ => - et / => _).
   *
   * @param string $value
   *   Chaîne à encoder.
   *
   * @return string
   *   La chaîne encodée
   */
  private function encodeBase64UrlSafe($value) {

    $base64_value = base64_encode($value);
    return str_replace(['+', '/'], ['-', '_'], $base64_value);
  }

  /**
   * Décode une chaîne en base 64 dont les caractères gênants sont échappés.
   *
   * @param string $value
   *   Chaîne à décoder.
   *
   * @return string
   *   La chaîne décodée
   */
  private function decodeBase64UrlSafe($value) {

    $value = str_replace(['-', '_'], ['+', '/'], $value);
    return base64_decode($value);
  }

}

Pour récupérer les limites de la ville de Paris, on aura par exemple :

$api_key = '<MON API KEY>';
$city = 'Paris';
$postal_code = 75000;
$geocoder = new Geocoder($api_key);
$geocoder->getCity($city . ' ' . $postal_code);

Remarques :

  • Cette classe a été créée initialement pour récupérer les limites de toutes les villes de France. Elle permet toutefois de construire l'appel Google pour géocoder n'importe quelle adresse.
  • La classe permet de gérer un compte client Google Map, permettant de dépasser les 2000 requête/jours. La classe a été réécrite pour l'API Google Map v3. Cette fonctionnalité n'a pas été retestée dans cette version.

Astuce Utiliser Google Analytics pour un Intranet

Google Analytics fonctionne très bien même en intranet, tant que les utilisateurs du site ont accès à internet. Cela n'est vrai qu'à une condition : le site intranet doit avoir un nom de domaine finissant par ".qqchose" (.fr, .com, ...).

Par exemple, http://mon-intranet.fr, http://mon-intranet.pwet fonctionneront, mais pas http://mon-intranet.

Dans ce cas particulier, vous pouvez quand même utiliser Google Analytics en ajoutant cette ligne au code javascript fourni par Google :

_gaq.push(['_setDomainName', 'none']);

Marque-page Utiliser l'API Google Analytics en PHP

Pour pouvoir utiliser les données de Google Analytics directement en PHP, il existe une petite api pour communiquer simplement avec Google : GAPI.

En deux lignes de code, vous pouvez ainsi récupérer le nombre de visualisations par page pour les 30 derniers jours :

$ga = new gapi('address@server.com', 'my_password');

// Récupération du nombre de visualisations, pour chaque couple (url de la page, titre de la page)
$ga->requestReportData('71538601', ['pagePath', 'pageTitle'], ['pageviews']);

Astuce Grouper des points sur une Google Map

Lorsque vous affichez une Google Map avec beaucoup de points rapprochés, ils risquent de se chevaucher. Pour éviter ce problème, vous pouvez les regrouper en utiliser le MarkerClustering.

L'autre avantage est le temps de chargement de la carte. Puisqu'il y a des regroupements, il y a moins de points à afficher.

Voici un exemple : Exemple de Marker clustering

À gauche une carte avec beaucoup de points et à droite, la même avec des regroupements.

Pour utiliser le regroupement, il suffit de créer la carte et les points, mettre les points dans un tableau et créer un nouvel objet MarkerClusterer.

Voici un aperçu du code à ajouter pour créer des points regroupés :

// Soit data un tableau d'objets ayant des latitudes et longitudes
var markers = [];
for (var i = 0; i < 100; ++i) {
    var marker = new google.maps.Marker({
      "lat": data[i].lat,
      "long": data[i].long
    });
    markers.push(marker);
}
var markerCluster = new MarkerClusterer(map, markers);

Remarque : Il faut également ajouter le fichier markerclusterer.js à votre page.

Astuce [eZ4] Consommer des services web fournis par Play!

Voici une extension eZ Publish permettant de récupérer des webservices fournis par Play! : AT Play Connector.

Cette extension propose une application Play! d'exemple, correspondant aux exemples des articles précédents. Pour chaque service web de cette application, une méthode PHP et un fetch() permettent de récupérer son résultat côté eZ Publish.

Ces méthodes sont implémentées dans le fichier classes/ATWSFunctionCollection.php (à partir de la racine de l'extension), dont voici un extrait :

<?php

class ATWSFunctionCollection extends ATWebServices {

    /**
     * Appel du service web /test
     *
     * @param string $nom Nom des locataires recherchés
     * @param string $dateNaissance Date de naissance  des locataires recherchés
     * @param string $lang Langue des résultats
     */
    public static function test() {

        // Nom du service web
        $wsName = '/test';

        // Construction de la requête
        $wsRequest = $wsName;

        return array( 'result' => parent::getJSONResult( $wsRequest ) );
    }

    /**
     * Appel du service web /helloWorld
     *
     * @param string $name Nom de la personne à saluer
     */
    public static function helloWorld( $name ) {

        // Nom du service web
        $wsName = '/helloWorld';

        // Construction de la requête avec les paramètres
        $wsRequest = $wsName . '?';

        if ( $name != null ) {

            $wsRequest .= 'name=' . urlencode( $name );
        }

        return array( 'result' => parent::getJSONResult( $wsRequest ) );
    }
}

Explications :

  • Cette classe étend la classe ATWebservices également fournie par l'extension.
  • Chaque méthode ne fait que préparer le début d'une requête HTTP, avec le nom du service web et les paramètres à envoyer.
  • Ces requêtes HTTP sont complétées et soumises au services web de Play! via la méthode getJSONResult() de la classe ATWebServices.

Le fichier modules/atplayws/function_definition.php (à partir de la racine de l'extension) déclare des fetch() utilisables depuis les templates :

<?php

$FunctionList = array( );

$FunctionList['test'] = array(
        'name' => 'test',
        'operation_types' => array( 'read' ),
        'call_method' => array(
                'class' => 'ATWSFunctionCollection',
                'method' => 'test' ),
        'parameter_type' => 'standard',
        'parameters' => array( )
);

$FunctionList['hello_world'] = array(
        'name' => 'hello_world',
        'operation_types' => array( 'read' ),
        'call_method' => array(
                'class' => 'ATWSFunctionCollection',
                'method' => 'helloWorld' ),
        'parameter_type' => 'standard',
        'parameters' => array(
                array( 'name' => 'name',
                        'type' => 'string',
                        'required' => true ),
        )
);

Explications :

Pour chaque fetch, on déclare son nom, la méthode PHP correspondante à appeler, ainsi que les paramètres à lui fournir.

Remarque :

Pour plus de détails sur l'utilisation de l'extension, consultez le fichier README.txt présent à sa racine.

Astuce Créer des services web avec Play!

Tracer sa route

Play! vous permet de créer des services web accessibles via de simples requêtes HTTP.

Vous devez pour cela définir des routes, pour que l'application sache quelle méthode exécuter en fonction de la requête HTTP.

C'est le rôle du fichier conf/route, dont voici un exemple :

# Routes
# Ce fichier définit les différentes routes (avec les routes prioritaires en premier)
# ~~~~

# Mappe les fichiers de ressources statiques du répertoire /public avec le chemin /assets dans l'URL
GET     /assets/*file          controllers.Assets.at(path="/public", file)

GET     /test                  controllers.Application.test()
GET     /helloYou              controllers.Application.helloYou(firstName, lastName)
GET     /userList              controllers.Application.userList()

Explications :

  • Pour chaque service web, on définit une route de la forme : <Type de requête (GET|POST)> <url> <package.Classe.method()>.

  • L'appel de l'URL http://localhost:9000/test retournera le résultat de la méthode test() de la classe Application du package controllers.

  • La méthode helloYou() nécessite les arguments firstName et lastName.
    Pour l'appeler, l'url sera http://localhost:9000/helloYou?firstName=Jean-Louis&lastName=David.
    Comme la requête est définie en GET, il suffit d'ajouter les paramètres dans l'url sous la forme nom_param=valeur.

Remarque :

La première route (GET /assets/*file...) est un peu particulière et sert au fonctionnement interne de Play!, pour gérer les fichiers statiques de l'application.

Une méthode qui a la classe (et vice-versa)

Voici un aperçu de la classe Java qui contient les méthodes définie dans le fichier route :

package controllers;

import java.util.Date;

import models.WSResult;
import models.beans.Bean;
import models.beans.MessageBean;
import play.libs.Json;
import play.mvc.Controller;
import play.mvc.Result;

public class Application extends Controller {

    public static Result test() {

        WSResult result = new WSResult();

        result.addBean(new Bean() {
            public String message = "Test"; 
            public Date date = new Date(); 
        });

        return ok(Json.toJson(result));
    }

    public static Result helloYou(String firstName, String lastName) {

        WSResult result;

        if (!firstName.equals("") && !lastName.equals("")) {
            result = new WSResult();
            result.addBean(new MessageBean("Hello " + firstName + " " + lastName + "!"));
        } else {
            result = new WSResult("Paramètres incorrectes.", WSResult.ERROR_CODE_CUSTOM_PARAMS);
        }

        return ok(Json.toJson(result));
    }
}

Explications :

  • La classe Application étend la classe Controller fournie par Play!.
  • Pour chaque URL définie dans le fichier route, on retrouve bien la méthode dans la classe Application, avec ses arguments. (Notez que les noms des arguments doivent être identiques.)
  • Chaque méthode est statique et retourne un objet Result (classe également fournie par Play!).
  • Dans cet exemple, à chaque Result est associée une liste d'objets Bean.
  • Un objet Bean regroupe des informations à rendre accessible via un service web (un message, le résultat d'un requête SQL, ...)
  • Dans cet exemple, la classe Result a été étendue par la classe WSResult, pour lui ajouter une liste d'objets Bean, un éventuel message d'erreur, et d'autres informations.
  • Chaque méthode retourne un objet Result en JSON, via la méthode ok(Json.toJson(result)) fournie par Play!.
  • La seconde méthode vérifie si les paramètres sont vides. Si oui, alors l'objet Result ne contiendra pas de Bean mais un message et un code d'erreur. Si non, il contiendra un Bean comme dans la première méthode.

Un vrai Bean, monsieur

Normalement, un Bean représente une ligne de données provenant d'une source (base de données, fichier, ...). C'est une classe Java des plus simples, avec juste des attributs et leurs getter() et setter().

Imaginons par exemple qu'on veuille représenter un utilisateur de site web présent dans une table de base de données, on pourrait avoir :

package models.beans;

public class UserBean extends Bean {

    private int userID;
    private String email;
    private String login;
    private String password;

    public static final String FIELD_USER_ID  = "user_id";
    public static final String FIELD_EMAIL    = "email";
    public static final String FIELD_LOGIN    = "login";
    public static final String FIELD_PASSWORD = "password";

    public int getUserID() {
        return userID;
    }

    public void setUserID(int userID) {
        this.userID= userID;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getLogin() {
        return login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

Explications :

  • On étend la classe Bean, car la classe WSResult attend des objets Bean dans sa liste.
  • En plus des attributs caractérisant un utilisateur, on ajoute des attributs publics et statiques pour préciser quels champs de la base de données correspondent.
  • Pour que Play! puisse se connecter à une base de données, vous devez procéder à quelques configurations.

Retour à la base

Une fois que la classe UserBean a été créée, il faut écrire dans la classe Application la méthode qui va consulter la base de données et retourner un objet Result :

package controllers;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import models.WSResult;
import models.beans.UserBean;
import play.libs.Json;
import play.mvc.Result;

public class Application extends ApplicationController {

    /**
     * Retourne la liste des utilisateurs de la base de données.
     * 
     * @return Result
     */
    public static Result userList() {

        WSResult result  = new WSResult();
        String queryName = "user_list";

        String query = ApplicationController.getQuery(queryName);

        PreparedStatement statement;
        try {

            statement = ApplicationController.getConnection().prepareStatement(query);
            ResultSet resultSet = statement.executeQuery();

            // Parcours des lignes retournées
            while (resultSet.next()) {

                UserBean bean = new UserBean();
                bean.setId(resultSet.getInt(UserBean.FIELD_ID));
                bean.setEmail(resultSet.getString(UserBean.FIELD_EMAIL));
                bean.setLogin(resultSet.getString(UserBean.FIELD_LOGIN));
                bean.setPassword(resultSet.getString(UserBean.FIELD_PASSWORD));

                result.addBean(bean);
            }

        } catch (SQLException e) {

            result = new WSResult("Erreur SQL.", WSResult.ERROR_CODE_SQL);
            result.setErrorCode(e.getErrorCode());
            result.setErrorMessage(e.getMessage());
        }

        return ok(Json.toJson(result));
    }
}

Explications :

  • La méthode utilise l'api java sql, pour préparer les requêtes et les exécuter.
  • On parcourt ensuite les lignes retournées par la source de données
  • Pour chaque ligne de résultat, on instancie un nouveau Bean (UserBean ici puisqu'il s'agit d'utilisateurs) et on l'ajoute à la liste de Beans de l'objet Result.
  • On retourne l'objet Result transformé en JSON.
  • En cas d'erreur SQL, on retourne un objet Result contenant le message et le code d'erreur SQL.

Stockage des requêtes

Dans la méthode ci-dessus, on ne voit aucun code SQL. Les requêtes sont stockées dans un fichier qui les regroupe toutes : conf/sql.properties.

Comme tout fichier .properties standard, chaque ligne est de la forme clé = valeur :

user_list = SELECT * FROM user
admin_user = SELECT * FROM user WHERE login = 'admin'

Ces requêtes sont récupérées dans les méthodes java à partir de leurs clés :

String queryName = "user_list";
String query = ApplicationController.getQuery(queryName);

Lancement des requêtes

Finalement, si vous tapez http://localhost:9000/userList dans votre navigateur, vous obtenez quelque chose de la forme :

Résultat JSON user list

En résumé :

  • Play! reçoit votre requête HTTP.
  • Il cherche dans son fichier route la méthode correspondante et l'appelle.
  • La méthode cherche la requête SQL demandée, l'exécute et parcours les résultats.
  • Ces résultats sont "transformés en Bean" et stockés dans un objet Result.
  • L'objet Result est transformé en JSON et Play! vous en retourne le flux.

Remarque : Si vous avez un affichage du JSON illisible, il existe une extension Firefox très utile.

Astuce Utiliser Play! Framework avec MySQL

Pour que Play! puisse se connecter à une base de données MySQL en Java, vous avez deux fichiers à modifier.

conf/application.conf

Voici un exemple de configuration avec une base de données MySQL de base dans WAMPServer.

# Database configuration
# ~~~~~ 
# You can declare as many datasources as you want.
# By convention, the default datasource is named `default`
#
# db.default.driver=org.h2.Driver
# db.default.url="jdbc:h2:mem:play"
# db.default.user=sa
# db.default.password=
#
# You can expose this datasource via JNDI if needed (Useful for JPA)
# db.default.jndiName=DefaultDS

db.default.driver=com.mysql.jdbc.Driver
db.default.url="jdbc:mysql://localhost:3306/database_name" 
db.default.user="root" 
db.default.password=""

Les 4 dernières lignes permettent de choisir :

  • le driver Java à utiliser,
  • l'adresse, le port et le nom de la base de données à laquelle se connecter
  • le login et le mot de passe de l'utilisateur à utiliser

project/Build.scala

import sbt._
import Keys._
import PlayProject._

object ApplicationBuild extends Build {

    val appName         = "MyApplication"
    val appVersion      = "1.0-SNAPSHOT"

    val appDependencies = Seq(
     // Dépendances du projet :
     "mysql" % "mysql-connector-java" % "5.1.20"
    )

    val main = PlayProject(appName, appVersion, appDependencies, mainLang = JAVA).settings(
      // Configuration du projet :     
    )

}

La ligne à ajouter est : "mysql" % "mysql-connector-java" % "5.1.20"

Pour que Play! prenne en compte ces modifications (en particulier la modification de Build.scala), vous devez relancer Play! et lui demander de recharger la configuration :

cd path/to/my/play/application
play
$ reload
$ update
$ run