Statamic Peak

Article

Démarrer avec Lumen (partie 2)

Suite de mon introduction à Lumen, avec cette fois un focus sur les controllers, middlewares et service providers.

Cet article date de 2016 et concerne Lumen 5.0

Dans mon précédent post sur Lumen, je vous ai parlé de son installation et du démarrage de project avec des subtilités comme l’ajout de fonctionnalités de base de Laravel. Nous avons également vu l’utilisation d’artisan pour créer sa base de données au travers des migrations et des seeders. Je vais allé plus loin aujourd’hui en parlant notamment de l’utilisation des controllers, des middlewares et des services providers.

Si vous êtes à la recherche d'informations  à jour, j'écris un compendium sur Laravel.

Controller

Je vous ai déjà présenté comment exécuter une fonction lorsqu’un utilisateur arrive sur une page de votre site grâce au fichier routes.php. Cette solution n’est pas forcément la plus propre ni la plus pérenne. En effet, on ne va pas laisser tout notre code dans un seul fichier. Pour résoudre ce problème, on va pouvoir créer des controllers, que l’on va appeler dans routes.

Dans l’exemple du précédent post, on a créé un model User que l’on a utilisé pour illustrer les fonctions de connexions de bases de Lumen. Imaginons que nous souhaitons afficher aux utilisateurs les informations personnelles que nous avons recueillies à leur sujet. Nous allons avoir besoin d’un controller contenant toutes les actions en rapport avec ces derniers.

Pour rappel, il suffit de faire  php artisan serve pour démarrer le serveur php inclut avec Lumen.

On va commencer en créant la route pour rendre accessible ses données :

<?php 
$app->post("/mail","UserController@data");

Dans un premier temps, nous allons utilisé une requête POST pour envoyé nos données. Nous verrons ensuite comment faire pour utiliser une requête GET à la place et mieux respecter la sémantique des verbes HTTP.

On va poursuivre en créant le fichier UserController :

<?php
namespace App\Http\Controllers;
use Laravel\Lumen\Routing\Controller;
class UserController extends Controller {}

On va alors pouvoir créer notre fonction pour accéder aux informations d’un utilisateur en fonction d’une adresse e-mail de départ. Pour se faire, on va utiliser l’object Request qui est envoyé par Lumen à chaque fonction à la suite d’une route. Ce dernier contient différentes informations sur la requête de l’utilisateur et notamment les données envoyées par ce dernier en GET / POST.

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use Laravel\Lumen\Routing\Controller;
class UserController extends Controller {
	public function data(Request $request) {
    	$email = $request->input("email");
        if(is_null($email)) return response()->json(false,401);
        return response()->json(User::where("email","=",$email)->get());
    }
}

Cette fonction va dans un premier temps récupérer les données fournies par la requête. On va ensuite vérifier qu’une adresse e-mail a effectivement été envoyée. Sinon, on retourne une erreur 401 et false comme corps de retour. Si on a bien reçu une adresse e-mail, on retourne l’intégralité des informations concernant un utilisateur.

Pour tester facilement vos API, je vous conseille Postman, c’est une application Chrome gratuite vraiment pratique qui permet d’interagir avec vos points d’accès en fournissant un contrôle total sur vos paramètres, etc. Je l’ai remplacé il y a peu par Paw que je trouve plus pratique et moins gourmand (je montais à 2go de RAM utilisée par Postman dans certains cas extrêmes), mais uniquement sous Mac et payant ($30).
{
	"id": 1,
    "email": "claude@gmail.com",
    "password": "$2y$10$K3E3drd5X7s50eHui5hG3uJxj8TMkZObZKbwh4OIsKGv6ayHcdWi2",
    "remember_token": "",
    "created_at": "2015-10-03 15:55:26",
    "updated_at": "2015-10-03 15:55:26"
}

Malheureusement, on envoie un certain nombre de données sensibles et d’autres peu intéressantes pour l’utilisateur. Pour éviter cette fuite de données, on va pouvoir modifier notre modèle User en lui fournissant un tableau d’attributs qui ne doivent pas être envoyés.

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
final class User extends Model {
	protected $hidden = ["id",'password','created_at','updated_at'];
}

De cette façon, tous les champs présents dans le tableau $hidden ne seront jamais présents quand le modèle User est transformé en tableau associatif.

 { "email": "claude@gmail.com", "remember_token": "" } 

Nos données sont alors protégées !

Si l’on souhaite respecter la sémantique des verbes HTTP, il convient d’utiliser une requête GET ici plutôt que POST. En effet, notre objectif est de récupérer des données et non de les modifier. Cependant, de manière générale, le corps d’une requête (ici le mail) n’est pas envoyé dans le cas d’un GET, il faut donc le récupérer d’une autre façon. Le cas classique en PHP est l’envoi de données via l’URL et plus particulièrement la query string.

C’est par exemple le cas d’un formulaire HTML en method = "GET" où nos données vont être envoyées à l’URL http://www.example.com/form.php?name=test&action=login. Lumen nous permet un format d’URL un peu plus sexy de la forme http://www.example.com/login/test.

Dans le cas de notre récupération d’informations client, on va pouvoir proposer une URL bien construite de la forme http://localhost/infos/claude@gmail.com.

Pour se faire, il nous suffit de modifier la route définie dans routes.php.

<?php
$app->get("/infos/{email}","UserController@data");

Afin d’accéder à cette donnée, nous ne pouvons plus utiliser $request->input("email"), une variable est automatiquement créée pour nous et passée à la fonction du controller :

<?php
public function data($email) {
	if(is_null($email)) return response()->json(false,401);
    return response()->json(User::where("email","=",$email)->get());
}

Middleware

Un middleware est une fonction qui est appliquée à chacune des requêtes avant leur arrivée dans les controllers. Le plus classique est la gestion des comptes, où l’on peut autoriser l’accès à certains fonctions uniquement pour une personne connectée. C’est un élément présent de base dans Laravel, qui ne l’est pas dans Lumen, donc on va devoir le rajouter à la main et ça va nous faire un bon exemple !

On va créer le fichier Authenticate.php dans le dossier Http/Middleware :

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate { 
	/*
    * * The Guard implementation. 
    * * @var Guard 
    */ 
    protected $auth; 
    
    /*
    * * Create a new filter instance. 
    * * @param Guard $auth 
    * @return void 
    */ 
    public function __construct(Guard $auth) {
    	$this>auth = $auth;
    } 
    
    /*
    * * Handle an incoming request. * 
    * @param \Illuminate\Http\Request $request 
    * @param \Closure $next 
    * @return mixed 
    */ 
    public function handle($request, Closure $next) {
    	if ($this->auth->guest()) {
        	if ($request->ajax()) {
            	return response('Unauthorized.', 401);
            } else { 
            	return redirect('auth/login');
            }
        } return $next($request);
    }
}

Il suffit ensuite de modifier le fichier bootstrap/app.php pour définir notre middleware :

<?php
$app->routeMiddleware([
	'auth' => \App\Http\Middleware\Authenticate::class
]); // ligne 66-67

On peut ensuite appeler notre middleware depuis routes.php :

<?php
$app->group(['middleware' => 'auth'], function ($app) {
	$app->post("/mail","UserController@data");
});

Toutes les routes présentes dans notre groupe seront uniquement accessibles par un utilisateur connecté. Dans le cas contraire, il sera redirigé vers auth/login ou recevra une 401 dans le cas d’une API.

Pour vérifier que tout marche bien, on va reprendre la fonction de connexion qu’on a créé la dernière fois. On va la placer dans le controller :

<?php
public function co() { 
	if (\Auth::attempt(array("email"=>"claude@gmail.com","password"=>"jean"))) { 
    	return "It works";
    } else {
    	return response()->json(\Auth::check());
    }
}

Il ne nous reste plus qu’à l’appeler dans routes.php, en dehors du groupe :

<?php
$app->get("/co", "UserController@co");

En appelant http://localhost:8000/co, on obtient le message It works, on est alors connecté via le système de Lumen. Si on appelle maintenant http://localhost:8000/mail on retrouve bien les informations du compte et non l’erreur 401. Il ne nous manque plus que la déconnexion et on aura un système complet !

On ajoute donc la fonction de déconnexion à notre controller :

<?php 
public function deco() {
	return response()->json(\Auth::logout());
}

Puis la route, dans le groupe cette fois :

<?php
$app->get("/deco","App\Http\Controllers\UserController@deco");

On peut alors se déconnecter, avec http://localhost:8000/deco. A ce moment, on n’a plus accès à nos informations personnelles !

Service Provider

Utiliser un service provider est un bon moyen d’introduire une classe existante, ou une librairie, au sein de votre application Laravel. L’idée est de créer une instance une fois et de "promener" cette instance dans notre application.  Par exemple, si l’on souhaite gérer l’envoi de mails via un prestataire externe (comme SendinBlue), on va devoir inclure le fichier source de leur bibliothèque et créer une instance de leur classe en lui passant une clé d’API.

J’ai pour habitude de placer ces classes / librairies dans un dossier Services afin de m’accorder avec le terme Service Provider, mais je ne pense pas qu’il y ait de règle précise.

Il nous suffit de créer un fichier MailProvider dans le dossier app/Providers :

<?php
namespace App\Providers;
require_once "../app/Services/mailin.php";
use Illuminate\Support\ServiceProvider;
use \stops as stops;
class MailProvider extends ServiceProvider { 
	/*
    * * Register bindings in the container. 
    * * @return void 
    */ 
    public function register() { 
    	$this->app->singleton('App\Services\Mails', function($app) {
        	return new Mailin("https://api.sendinblue.com/v2.0","Your access key"); 
        });
    }
}

Notre fichier contient une classe avec une fonction unique register(), c’est dans cette dernière que nous allons pouvoir enregistrer notre service. Nous pouvons choisir entre créer un singleton ou une nouvelle instance de la classe (via $this->app->bind à la place de $this->app->singleton).

Il ne nous reste plus qu’à ajouter notre service provider dans bootstrap/app.php :

<?php 
// dans la section "Register Service Providers"
$app->register('App\Providers\MailProvider'); 

On peut alors récupérer une instance, ou un singleton, dans nos controller via la fonction make :

<?php 
public function __construct() { 
	$this->mails = \App::make('App\Services\Mails');
}

Par la suite, on va pouvoir utiliser $this->mails comme une instance classique de Mailin.

Les services providers sont au final assez facile à utiliser et permettent une meilleure séparation avec les classes externes à Lumen.