Un service REST avec Laravel

Laravel 4 nous fournit un générateur de contrôleur de ressource REST. Avec la commande

php artisan controller:make PhotoController

J’obtiens bien un fichier PhotoController.php dans le dossier /app/controllers/. Mais ce squelette et très généraliste alors que l’on me parle de contrôleur de ressource dans la documentation : http://laravel.fr/docs/4/controllers#resource-controllers. Perso je voie en fait plus un contrôleur REST général. Moi ce qui m’intéresse c’est de faire un service REST. Un service est généralement(en ce moment) au format JSON, avant-hier c’était le xlm et demain ?

Donc, mon contrat de base est de faire un contrôleur générique JSON. Et, pour faire simple :) je pense à utiliser le pattern Repository.

 

1) Logiciels

Petite aparté, Il est impossible de tester complètement un service Rest, et oui il faut utiliser des commandes HTTP post,put et delete. curl est une possibilité; avec Laravel, il est possible d’utiliser ces verbes HTTP, mais il faut écrire ces propres routes, formulaires. Généralement ces services Rest sont plutôt utilisés avec du javascript, mais vous n’avez peut-être pas encore écrit le client particulié :(;.
Pas grave, il existe des extensions Firefox et Chrome qui sont de très bon clients :). L’image ci jointe représente une application Chrome : Postman REST Client. Mais il en existe bien d’autre, comme « Advanced Rest Client ».

test d'un service REST

2) La gestion du JSON

Autant essayer de laisser toute la gestion Json a une classe particulière. Je vais donc me faire un petit contrôleur de base ResourceController extends Controller. Peut-être qu’un nom comme ResourceJsonController serait plus judicieux ? Mais, chaque service a son propre format, et ici nous allons définir un format type, donc le bon nom serait ResourceMonFormatJsonController. Mon service ne retournera qu’un seul format, et il sera du type :

  • NomduService
  • NoErreur : 0 pas d’erreur
  • Message : message d’erreur ou général de log
  • Datas : tableau de données, les datas du service

Avec ce type de format, on vérifie dans le résultat si l’erreur est égale à zéro, et uniquement si c’est bien le cas, on traite, affiche les données qui sont dans la propriété datas. Voici un  exemple de résultat avec ce format Json.

{
    "error": 0,
    "message": "PaysController::postFind",
    "service": "LesPaysDuMonde",
    "data": [
        "Allemagne",
        "Espagne",
        "Pologne",
        "régions éloignées de l’Océanie"
    ]
}

passons a notre classe de base app/controllers/ResourceController.php

<?php //app/controllers/resourceController.php
abstract class ResourceController extends Controller
{    
   protected $result = null;

   abstract public function getService();

La propriété $this->result est l’objet qui sera retourné par le service au format Json.
La méthode getService() retourne tout simplement une chaine dans la propriété « service » de notre objet Json.

Le format, et l’objet réponse est généré dans le constructeur

    /**
     *  formate la reponse par defaut
     **/
    public function __construct()
    {
        $this->result= new stdClass();
            $this->result->error= 0;
            $this->result->message= '';
            $this->result->service= $this->getService();
            $this->result->data = null;
    }

Pour une meilleure indépendance, il faudrait créer une classe JsonReponse, mais pour moi, ce format est assez générique pour me suffire dans 100% de mes cas.

public function __construct(JsonReponse $reponse)
    {
        $this->result= $reponse;
    }

Une méthode render(), la plus importante :)

protected function render()
    {
        return Response::json($this->result);
    }

Une petite méthode d’erreur automatique au cas où.

public function missingMethod($parameters)
    {
        $this->result->err=404;
        $this->result->message='Service '.$this->getService().' : '.$parameters. ' non disponible';
        return $this->render();
    }

Pour finir, LA méthode qui sera utilisée par toutes les méthodes des classes descendantes. Cette méthode remplie en particulier le résultat en cas d’exception(erreur) de la  fonction anonyme appelée par la classe descendante.

protected function run($function)
    {
        try {
            $this->result->data= call_user_func($function);
        } catch (Exception $e) {
            $this->result->err = ($e->getCode()>0) ? $e->getCode() : -1;
            $this->result->message = $e->getMessage();
        }
        return $this->render();
    }

Pour exemple, une méthode d’une classe descendante de ResourceController:

public function /*MonJsonController.*/destroy($id) {
	return $this->run( function() use ($id)
        {
            return $this->datas->delete($id);
	});
}

2) Repository

Je suis pas doué, pour simplifier … je vais rajouter une class Repository :). Un repository est une classe qui se situe entre le Model et le Contrôleur. Elle concerne en particulier tous les accès au Modèle; le modèle, lui ne conservant que le format des datas.

Pourquoi utiliser les repositories ?

  • Nous partons d’une nouvelle version de Laravel (aujourd’hui 3 mois), donc nous n’avons pas de vieux code a réutiliser, pourquoi ne pas alors partir avec des bonnes résolutions ? Un code plus propre, plus clair, plus maintenable.
  • Des gens très compétents ont inventés, utilisent ce design pattern très utilisé; qui suis-je pour les contredire ?
  • Avec les repositories, le code de mes contrôleurs devient plus clair :) : plus de code sql-orm.
  • Avec les repositories, mon code est beaucoup plus portable : je passe facilement de mysql a redis, aux fichiers texte xml …
  • Avec les repositories, les test unitaires sont simplifiés.

Dans mon cas, ici je parle de « Services », je peux très bien avoir un service s’appuyant sur une Base noSql, un autre sur MySql et enfin d’autres sur des fichiers ini, xlm. Dans cette optique, les contrôleurs seront identiques quelque soit le support des données.
Si un service doit changer de support, ce sera beaucoup plus simple à réécrire le service.

Nos données

Il nous faut des données pour alimenter notre service. J’ai pris sur GitHub une base de données « country » https://github.com/umpirsky/country-list/tree/master/country/icu/fr Il est difficile de faire plus simple : id (‘fr’) et name (‘France’), mais je pense que tout le monde a une table users, produit sur sa machine.

<?php // app/models/Country.php
/*
* id varchar(2) 
* name varchar(64)
*/
class Country extends Eloquent
{
    protected $table = 'countries';
    public $timestamps = false;
}

Stockage des données

Notre repository doit avoir les accès à notre modèle, pour pouvoir passer d’un stokage à un autre, nous utilisons une interface :

interface ICountryRepository
{
  public function all();
  public function find($id);
  // ... update, delete, ....
}

Ici, je n’utilise que Eloquent

<?php // /app/models/CountryRepository.php

class CountryRepository implements ICountryRepository
{
    public function all()
    {
      return Country::all();
    }

    public function find($id)
    {
      $country=Country::find($id);
      return $country->getAttributes();
    }

    public function create($input)
    {
      return Country::create($input);
    }

    public function update($input)
    {
      $id= \Input::get('id');
      $country= ($id=='') ? Country::find($id): new Country() ;
      $country->name= \Input::get('name');
      return $country->save();
    }

    public function nouveau()
    {
      $country= new Country(); //return $country->getAttributes();
      return array('id'=>'','name'=>'') ;
    }

    public function delete($id)
    {
      Country::setSoftDeleting(true);
      return Country::delete($id);
    }

    public function chercher($texte)
    {
      return DB::select('select * from countries where name LIKE ? LIMIT 300;', array('%'.$texte.'%') );
    }

}

Vous ne voyez pas de validations, mais elles peuvent quand même exister par un ancêtre model particulier.

3) La route

Route::group(array('prefix' => 'services'), function()
{
	Route::resource('pays', 'PaysController');
	Route::post('pays/find','PaysController@postFind');
});

4) Le contrôleur

utilisons la commande :

php artisan controller:make CountryController

Nous avons écrit un beau controleur ResourceController, il serait dommage de ne pas l’utiliser, nous n’avons qu’a changer l’extends de notre classe.

<?php // app/Controllers/CountryController.php

use ICountryRepository as Country;

class PaysController extends ResourceController {

    protected $datas= null;

    public function __construct(Country $pays)
    {
        parent::__construct();
        $this->datas = $pays;
    }

    public function getService() { return 'LesPaysDuMonde'; }

Nous passons notre repository(Interface) au constructeur. Normalement je devrais créer un serviceProvider pour indiquer à Laravel quelle Classe charger avec l’Interface, ici pour faire plus simple(une classe de moins), j’ai déclaré cette liaison dans app/route.php.

use \CountryRepository;
App::bind('ICountryRepository', 'CountryRepository');

Maintenant nous avons nos datas dans la propriété $this->datas, le réponse json dans $this->result et toutes les méthodes d’accès à notre service sont déjà écrites par php artisan :)

Pour retrouver tous les pays :

	/**
	 * Display a listing of the resource.
	 * GET /resource
	 *
	 * @return Response
	 */
	public function index()
	{
        $this->result->message=__METHOD__;
		return $this->run( function() 
        {
            $adatas=array();
            foreach( $this->datas->all() as $data){
                $adatas[] = $data->name;
            }
            return $adatas;
		});
	}

On utilise, on apprécie notre ancêtre ResourceController avec la méthode parente $this->run pour injecter des données dans la propriété réponse $this->result->data.
Dans la fonction anonyme, l’utilisation du repository nous décharge de tous code propriétaire au stockage.

/**
     * POST /resource/find
     *  ?text=
     *
     */
    public function postFind()
    {
        $this->result->message=__METHOD__;
		return $this->run( function()  
        {
            $chaine= trim(\Input::get('text'));
            if (strlen($chaine)<3) throw new Exception('Chaine de recherche non valide',-2);
            $adatas=array();
            $recherches=$this->datas->chercher($chaine);
            foreach( $recherches as $data){
                $adatas[] = $data->name;
            }
            if (count($adatas)<1) throw new Exception('`*'.$chaine.'*` non trouvé',0);
            return $adatas;
		});        
    }

$this->postFind(), elle est particulière, car ne faisant pas partie du contrôleur type. Il a fallut créer une route particulière. Ici, j’abuse des exceptions car elles sont gérées automatiquement par la classe ancêtre.

/**
	 * Show the form for creating a new resource.
	 * GET /resource/create
	 *
	 * @return Response
	 */
	public function create()
	{
        return $this->debug($this->datas->nouveau()) ;
        $this->result->message=__METHOD__;
		return $this->run( function() 
        {
            $adatas=array();

            foreach( $this->datas->nouveau() as $key=>$data){
                $adatas[$key] = $data;
            }
            return $adatas;
		});
	}

	/**
	 * Store a newly created resource in storage.
	 * POST /resource
	 * 
	 * @return Response
	 */
	public function store()
	{
        $this->result->message=__METHOD__;
		$this->result->error= -1;
		return $this->render();
	}

	/**
	 * Display the specified resource.
	 * GET /resource/{id}
	 * 
	 * @param  int  $id
	 * @return Response
	 */
	public function show($id)
	{
        $this->result->message=__METHOD__. '('.$id.')';
		return $this->run( function() use ($id)
        {
            $maCountry = $this->datas->find($id);
            return $maCountry;
		});
	}

	/**
	 * Show the form for editing the specified resource.
	 * GET /resource/{id}/edit
	 * 
	 * @param  int  $id
	 * @return Response
	 */
	public function edit($id)
	{
        $this->result->message=__METHOD__. '('.$id.')';
		return $this->run( function() use ($id)
        {
            $maCountry = $this->datas->find($id);
            return $maCountry->name;
		});
	}

	/**
	 * Update the specified resource in storage.
	 * UPDATE /resource/{id}
	 *
	 * @param  int  $id
	 * @return Response
	 */
	public function update($id)
	{
        $this->result->message=__METHOD__. '('.$id.')';
		return $this->run( function() use ($id)
        {
            return $this->datas->update( Input::all() );
		});
	}

	/**
	 * Remove the specified resource from storage.
	 * DELETE /resource/{id}
	 *
	 * @param  int  $id
	 * @return Response
	 */
	public function destroy($id)
	{
        $this->result->message=__METHOD__. '('.$id.')';
		return $this->run( function() use ($id)
        {
            return $this->datas->delete($id);
		});
	}

}

Voila, nous devons avoir presque un controleur de service REST universel, car ici tout le code particulier est déporté (un peu) dans le contrôleur parent mais surtout dans le repository.

 

Share Button

Vous devriez aimer...

1 Response

  1. patrick dit :

    Il manque une gestion de l’authentification pour le saisie.