Laravel 6

Une API avec Laravel 6

J’ai un peu abordé les API dans mon cours sur Laravel 6 mais sans vraiment approfondir cet aspect, alors je vais à présent un peu m’y attarder. Mais d’abord une API c’est quoi ? De façon très formelle ça signifie Application Programming Interface. Le mot le plus important là dedans est sans doute le dernier : interface. Notre monde regorge d’interfaces en tout genre, en commençant par la télécommande pour la télévision. En informatique c’est plus ciblé. Généralement on utilise une API REST. Encore un acronyme qui signifie Representational State Transfer. C’est un standard qui définit des règles pour créer un Service Web. Et voilà encore une autre appellation ! On pourrait dire qu’un service web c’est une API disponible sur un réseau.

Plutôt que de développer la théorie de tout ça, que vous pouvez trouver facilement sur Internet, je vais plutôt donner dans cet article un exemple de réalisation avec Laravel.

REST

Il faut quand même qu’on s’attarde un peu sur REST. On peut dire que c’est un style d’architecture pour créer des API mais ce n’est pas la seule ! On a aussi SOAP ou GraphQL, cette dernière étant très prometteuse, j’ai lui ai d’ailleurs récemment consacré un article complet. Mais force est de constater que pour le moment REST est très largement utilisé et sans doute pour encore pas mal d’années.

REST a plusieurs caractéristiques importantes dont la principale est qu’il est sans état (stateless). Ce qui signifie que chaque requête est indépendante des autres et que le serveur est toujours vierge par rapport à une requête. Si vous êtes habitué à utiliser Laravel de façon classique vous bénéficiez des sessions qui permettent de mémoriser au niveau du serveur une authentification. Ce n’est pas le cas avec une API REST, pour chaque requête il faut montrer patte blanche et prouver qu’on a le droit d’accéder aux ressources.

D’autre part on va parler de ressources auxquelles on accède avec des URI et un corps généralement en JSON, que Laravel sait très bien gérer. On va aussi utiliser le protocole HTTP puisqu’on est sur Internet avec ses méthodes GET, POST, PUT… On a un client et un serveur bien distincts.

Faisons un premier point des méthodes disponibles :

  • GET : on récupère tous les éléments ou un élément de la ressource (idempotente et cachable)
  • POST : on crée un élément dans la ressource (cachable)
  • PUT : on remplace un ou plusieurs éléments par une nouvelle version(idempotente)
  • PATCH : on met à jour un ou plusieurs éléments (on crée au besoin)
  • DELETE : on supprime un ou plusieurs éléments de la ressource (idempotente)

Indempotente est un mot un peu compliqué qui signifie seulement qu’on peut envoyer la requête plusieurs fois en ayant toujours le même résultat.

On crée notre API

Assez de théorie ! On va créer une API. On va commencer par installer Laravel :

composer create-project laravel/laravel monapi --prefer-dist

Comme exemple d’API on va prendre quelque chose de simple avec une liste de tâches. Ça sera complémentaire avec un exemple de réalisation côté client que j’ai montré sur mon autre blog.

On va créer le modèle et la migration :

php artisan make:model Todo -m

On trouve la migration ici :

On va la compléter :

public function up()
{
    Schema::create('todos', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('text', 255);
        $table->boolean('completed');
        $table->timestamps();
    });
}

On a donc deux champs :

  • text : le texte de la tâche
  • completed : un booléen pour savoir si la tâche a été accomplie

Pour les besoins de nos essais on va un peu remplir la table, il nous faut donc un seeder :

php artisan make:seeder TodoTableSeeder

Et pour créer les tâches un factory :

php artisan make:factory TodoFactory --model=Todo

On complète le code du factory :

$factory->define(Todo::class, function (Faker $faker) {
    return [
        'text' => $faker->sentence(),
        'completed' => $faker->boolean()
    ];
});

On complète aussi le code du seeder pour générer 10 tâches :

use App\Todo;

...

public function run()
{
    factory(Todo::class, 10)->create();
}

Et aussi on prévoit le lancement de notre seeder dans DatabaseSeeder.php :

public function run()
{
    $this->call(TodoTableSeeder::class);
}

On va préciser dans le fichier .env les identifiants de la base :

DB_DATABASE=monapi
DB_USERNAME=root
DB_PASSWORD=

On peut alors lancer migration et population :

php artisan migrate --seed

Si tout se passe bien on se retrouve avec les 10 tâches dans la table todos :

On va aussi créer un contrôleur de ressource :

php artisan make:controller TodoController --api --model=Todo

Le fait d’utiliser l’option –api exclut les méthodes create et edit. On a ainsi une belle trame pour notre code :

<?php

namespace App\Http\Controllers;

use App\Todo;
use Illuminate\Http\Request;

class TodoController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Todo  $todo
     * @return \Illuminate\Http\Response
     */
    public function show(Todo $todo)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Todo  $todo
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Todo $todo)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Todo  $todo
     * @return \Illuminate\Http\Response
     */
    public function destroy(Todo $todo)
    {
        //
    }
}

Il ne nous manque plus que les routes dans routes/api.php :

Route::apiResource('todos', 'TodoController');

Pour le moment je n’ai pas sécurisé l’accès, on verra cet aspect plus tard.

Les requêtes

On va maintenant faire fonctionner notre API pour les 5 routes présentes.

index

Pour l’URI api/todos avec le verbe GET, ce qui correspond à la route todos.index, on doit obtenir la liste complète des tâches, donc dans le contrôleur :

public function index()
{
    return Todo::all();
}

Eloquent comprend qu’on veut du JSON, ce qui nous arrange. On se retrouve avec ce contenu de réponse :

[
    {
        "id": 1,
        "text": "Et dicta ea minus adipisci.",
        "completed": 0,
        "created_at": "2020-02-06 17:38:53",
        "updated_at": "2020-02-06 17:38:53"
    },
    {
        "id": 2,
        "text": "Ratione soluta ipsam voluptatum recusandae doloremque itaque eligendi.",
        "completed": 1,
        "created_at": "2020-02-06 17:38:53",
        "updated_at": "2020-02-06 17:38:53"
    },
    {
        "id": 3,
        "text": "Quis esse sit est temporibus in libero.",
        "completed": 0,
        "created_at": "2020-02-06 17:38:53",
        "updated_at": "2020-02-06 17:38:53"
    },
...

store

Pour l’URI api/todos avec le verbe POST, ce qui correspond à la route todos.store, on doit créer une ressource, donc dans le contrôleur :

public function store(Request $request)
{
    Todo::create($request->all());
}

Mais pour que ça fonctionne il faut renseigner le modèle Toto des colonnes qu’on autorise en assignation de masse :

class Todo extends Model
{
    protected $fillable = [
        'text', 'completed',
    ];
}

On va tester ça :

Si ça ne passe pas avec votre client REST pensez à passer en application/x-www-form-urlencoded.

La nouvelle tâche a bien été ajoutée.

show

Pour l’URI api/todos/{id} avec le verbe GET, ce qui correspond à la route todos.show, on doit renvoyer une ressource, donc dans le contrôleur :

public function show(Todo $todo)
{
    return $todo;
}

On va retourner la tâche en JSON :

{
    "id": 11,
    "text": "Ma tâche",
    "completed": 0,
    "created_at": "2020-02-07 13:11:09",
    "updated_at": "2020-02-07 13:11:09"
}

update

Pour l’URI api/todos/{id} avec le verbe PUT ou PATCH, ce qui correspond à la route todos.update, on doit modifier une ressource, donc dans le contrôleur :

public function update(Request $request, Todo $todo)
{
    $todo->update($request->all());
}

delete

Pour l’URI api/todos/{id} avec le verbe DELETE, ce qui correspond à la route todos.delete, on doit supprimer une ressource, donc dans le contrôleur :

public function destroy(Todo $todo)
{
    $todo->delete();
}

Ça fonctionne, la tâche a disparu de la base.

Les ressources d’API

On a vu ci-dessus qu’il est facile de retourner une réponse JSON à partir des données d’un modèle. C’est parfait tant qu’on veut une simple sérialisation des valeurs des colonnes. Mais parfois on veut effectuer des traitements intermédiaires (ça correspond au design pattern transformer). Dans ce cas il faut créer une ressource d’API :

php artisan make:resource Todo

On obtient ce code de base :

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class Todo extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return parent::toArray($request);
    }
}

En gros on veut transformer un modèle en un tableau qui sera ensuite transformé en structure JSON. Avec le code de base on obtient exactement la même chose que ce qu’on a obtenu précédemment.

Pour utiliser la ressource il faut l’utiliser au niveau du contrôleur. Par exemple pour show :

use App\Http\Resources\Todo as TodoResource;

...

public function show(Todo $todo)
{
    return new TodoResource($todo);
}

Si on utilise maintenant la route api/todos/{id} on va retourner la même chose que vu plus haut. Maintenant si on veut éviter d’envoyer certains attributs il suffit de préciser ceux qu’on veut dans la ressource :

public function toArray($request)
{
    return [
        'text'=> $this->text,
        'completed'=> $this->completed,
    ];
}

Là on va retourner seulement ces deux attributs :

{
    "data": {
        "text": "Et dicta ea minus adipisci.",
        "completed": 0
    }
}

On peut aussi modifier les valeurs au besoin :

public function toArray($request)
{
    return [
        'text'=> $this->text,
        'completed'=> $this->completed ? 'true' : 'false',
    ];
}

Si on veut une collection de modèles la ressource offre la méthode collection, par exemple pour index :

public function index()
{
    return TodoResource::collection(Todo::all());
}

On va ainsi retourner :

{
    "data": [
        {
            "text": "Et dicta ea minus adipisci.",
            "completed": "false"
        },
        {
            "text": "Ratione soluta ipsam voluptatum recusandae doloremque itaque eligendi.",
            "completed": "true"
        },
...

Mais il est aussi possible de créer une ressource collection :

php artisan make:resource TodoCollection

Dans le contrôleur on fait appel à cette ressource collection :

use App\Http\Resources\TodoCollection;

...

public function index()
{
    return new TodoCollection(Todo::all());
}

On peut de cette manière gérer des relations ou ajouter toutes les données qu’on veut.

Il y a une documentation très complète sur le sujet.

Une authentification basique

Pour sécuriser une API il faut une authentification des utilisateurs. Laravel en propose une par défaut. On assigne à chaque utilisateur inscrit un token aléatoire. Ce token est ensuite utilisé pour chaque requête.

Créer des tokens

On va déjà ajouter une colonne à la table users pour mémoriser le token avec une migration :

php artisan make:migration add_token_to_users_table --table=users

On complète le code :

public function up()
{
    Schema::table('users', function ($table) {
        $table->string('api_token', 80)->after('password')
                            ->unique()
                            ->nullable()
                            ->default(null);
    });
}

On lance la migration :

php artisan migrate

On vérifie dans la table :

On va créer des utilisateurs avec leur token. On va compléter le factory UserFactory pour ajouter ce token :

$factory->define(User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
        'api_token' => Str::random(80),
    ];
});

On crée un seeder pour les users :

php artisan make:seeder UsersTableSeeder

On complète le code pour créer 3 utilisateurs :

public function run()
{
    factory(App\User::class, 3)->create();
}

On renseigne DatabaseSeeder :

public function run()
{
    $this->call(TodoTableSeeder::class);
    $this->call(UsersTableSeeder::class);
}

On lance la population (vous pouvez commenter la population des tâches pour éviter d’en créer à nouveau) :

php artisan db:seed

Protéger les routes

On va maintenant protéger les routes pour qu’elles ne soient plus accessibles sans authentification. Laravel est équipé d’un middleware qu’il suffit d’ajouter :

Route::middleware('auth:api')->group(function() {
    Route::apiResource('todos', 'TodoController');
});

On voit que le middleware a été ajouté aux 5 routes.

Maintenant si vous voulez accéder à ces routes vous allez tomber sur une erreur 500 parce que Laravel fait une redirection sur le login et qu’on n’en a pas prévu, mais le plus important c’est que la protection est en place.

Maintenant si j’ajoute un des tokens dans l’url :

Cette fois j’ai bien accès à la ressource.

On peut aussi créer une nouvelle tâche en transmettant le token avec les autres paramètres :

Cette protection est simple à mettre en œuvre mais évidemment elle est assez légère. Pour parfaitement sécuriser une API il vaut mieux utiliser Laravel Passport, mais OAuth 2.0 ne se laisse pas si facilement apprivoiser, même avec ce beau package.

JWT

Json Web Token (JWT) est un standard très utilisé. Il permet des échanges d’informations sécurisés à base de JSON. Il est utilisé par de multiples langages, et donc aussi PHP. Un jeton est signé et est constitué de 3 parties :

  1. Entête (header) : qui contient des métadonnées comme le mode de cryptage
  2. Contenu (payload) : le JSON
  3. Signature : les deux premières parties hashée avec une clé secrète

De cette manière si on modifie ce jeton ça va se voir parce qu’il n’est pas possible de récréer une signature valide sans la clé secrète.

Pour Laravel il existe un excellent package pour le mettre en place.

On va commencer par l’installer :

composer require tymon/jwt-auth:1.0.0-rc.5.1

J’ai été pobligé de préciser la version parce qu’il ne veut pas s’installer sur la dernière version de Laravel 6.

Et ensuite publier la configuration :

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Et on termine en générant la clé secrète :

php artisan jwt:secret

Dans le fichier .env on trouve cette clé :

JWT_SECRET=RQQk7aY2HPBupFdOSCBvx4KyZuWq5FF9vEPoicCUJkk2JnJnnaMMzKN9fH3Vg0ME

Bon là elle n’est plus vraiment secrète !

Ensuite il faut un peu modifier le modèle User :

use Tymon\JWTAuth\Contracts\JWTSubject;

...

class User extends Authenticatable implements JWTSubject
{
    ...

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

Et on va déclarer notre nouvelle authentification dans config/auth.php :

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

...

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

On passe en api par défaut avec jwt. Si vous voulez faire coexister plusieurs systèmes d’authentification, par exemple le web classique et api, vous pouvez laisser web par défaut ici mais il faudra préciser le guard pour l’authentification avec l’api.

On crée un contrôleur pour l’authentification :

php artisan make:controller Auth/AuthController

Avec ce code :

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login()
    {
        $credentials = request(['email', 'password']);

        if (! $token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth()->user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth()->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth()->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }
}

Comme précisé plus haut, si vous n’avez pas l’authentification api par défaut il faudra préciser dans le code du contrôleur à chaque fois, par exemple :

if (! $token = auth('api')->attempt($credentials)) {

Et enfin quelques routes pour accéder à ce contrôleur :

Route::middleware('api')->prefix('auth')->namespace('Auth')->group(function() {
    Route::post('login', 'AuthController@login');
    Route::post('logout', 'AuthController@logout');
    Route::post('refresh', 'AuthController@refresh');
    Route::post('me', 'AuthController@me');
});

On peut maintenant tenter un login en POST à l’adresse …/api/auth/login en transmettant en paramètre name et password :

On voit qu’on récupère un jeton access_token qu’on peut ensuite utiliser un peu comme on veut : paramètre dans l’url, cookie, paramètre POST, header, paramètre d’une route Laravel…

Les tâches revisitées

Reprenons l’exemple des tâches mais maintenant en distingant les utilisateurs. Dans la table todos on ajoute la clé étrangère pour les users :

public function up()
{
    Schema::create('todos', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('text', 255);
        $table->boolean('completed');
        $table->timestamps();
        $table->unsignedBigInteger('user_id');
        $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
    });
}

On complète le factory pour les tâches :

$factory->define(Todo::class, function (Faker $faker) {
    return [
        'text' => $faker->sentence(),
        'completed' => $faker->boolean(),
        'user_id' => random_int(1, 3),
    ];
});

On fixe bien l’ordre des populations :

public function run()
{
    $this->call(UsersTableSeeder::class);
    $this->call(TodoTableSeeder::class);
}

Et on prévoit maintenant 20 tâches :

public function run()
{
    factory(Todo::class, 20)->create();
}

Et on régénère la base :

php artisan migrate:fresh --seed

On utilise toujours JWT et on connecte un des utilisateurs créés :

On a maintenant son jeton :

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9tb25hcGkub29cL2FwaVwvYXV0aFwvbG9naW4iLCJpYXQiOjE1ODExODIyMjcsImV4cCI6MTU4MTE4NTgyNywibmJmIjoxNTgxMTgyMjI3LCJqdGkiOiI5TkhnQm5NcFFBV0VLV2xtIiwic3ViIjoxLCJwcnYiOiI4N2UwYWYxZWY5ZmQxNTgxMmZkZWM5NzE1M2ExNGUwYjA0NzU0NmFhIn0.F20rkZDrPUQ3hXt3XPZCM6tDLPBeaJiE-WYWv0CyByk

On ajoute la relation dans User :

public function todos()
{
    return $this->hasMany('App\Todo');
}

On modifie la méthode index de TodoController :

public function index()
{
    return new TodoCollection(auth('api')->user()->todos);
}

On peut alors lancer la requête :

monapi.oo/api/todos?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9tb25hcGkub29cL2FwaVwvYXV0aFwvbG9naW4iLCJpYXQiOjE1ODExODI4OTgsImV4cCI6MTU4MTE4NjQ5OCwibmJmIjoxNTgxMTgyODk4LCJqdGkiOiJUSk5PbkI3Qmd5cHlIODFxIiwic3ViIjoxLCJwcnYiOiI4N2UwYWYxZWY5ZmQxNTgxMmZkZWM5NzE1M2ExNGUwYjA0NzU0NmFhIn0.sThgEriRjGY-thcjBfkq5PI94h-7YbUgTDT5cJVTzrg

Avec comme résultat les tâches de l’utilisateur :

{
    "data": [
        {
            "text": "Vel consectetur fugiat quidem veritatis voluptas.",
            "completed": 0
        },
        {
            "text": "Voluptatum eos voluptatibus explicabo ex.",
            "completed": 0
        },
        {
            "text": "Amet nisi voluptatem voluptates aut.",
            "completed": 0
        }
    ]
}

Notre API est maintenant sécurisée !

Conclusion

On a vu dans cet article qu’il n’est pas difficile avec Laravel de créer et protéger une API.

 

Print Friendly, PDF & Email

8 commentaires

  • wazwilpyth

    Bonjour

    J’aurais une question SVP. Je mets en place une plateforme sous laravel et le projet comprends une partie WEB et la Partie API. J’ai essayé d’importer et utiliser dans mon API des MODELS de la partie WEB sans succès.

    Par exmple supposons que nous avons un controller MonPremierController.php (WEB) avec des models User et Facture,

    Et un controller ApiMonDeuxiemeController.php (API). Je ne parviens pas à utiliser le Model Facture dans le controller ApiMonDeux…

    json([‘success’ => $facture], $this-> successStatus);
    }
    }

    J’ai une erreur du style:

    [2021-11-15 11:47:05] local.ERROR: SQLSTATE[HY000] [1049] Unknown database ‘ma_base’ (SQL: select * from `factures`) {« userId »:1, »exception »: »[object] (Illuminate\\Database\\QueryException(code: 1049): SQLSTATE[HY000] [1049] Unknown database ‘ma_base’ (SQL: select * from `factures`) at /projet/vendor/laravel/framework/src/Illuminate/Database/Connection.php:664, PDOException(code: 1049): SQLSTATE[HY000] [1049] Unknown database ‘ma_base’ at /projet/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70)

    Vous avez une idée?

  • begood

    Bonjour,

    J’ai une question au sujet des paquets dans vendor, quels paquets pouvons-nous supprimer car jamais utilisés dans l’API ?

    Par défaut, y’en a moins de 50, mais dans le cadre de l’API, quel paquet faut-il conserver ?

    Merci.

  • Maitrepylos

    Salut, tu as une petite coquille en début d’article :

    Le fait d’utiliser l’option –api exclut les méthodes create et update. On a ainsi une belle trame pour notre code :

    C’est pas update mais edit 😉

    Toujours sympa tes tutos.

Laisser un commentaire