Tutoriel API Laravel : comment créer et tester une API RESTful

Publié: 2022-03-11

Avec l'essor du développement mobile et des frameworks JavaScript, l'utilisation d'une API RESTful est la meilleure option pour créer une interface unique entre vos données et votre client.

Laravel est un framework PHP développé avec la productivité des développeurs PHP à l'esprit. Écrit et maintenu par Taylor Otwell, le framework est très opiniâtre et s'efforce de faire gagner du temps aux développeurs en privilégiant la convention à la configuration. Le cadre vise également à évoluer avec le Web et a déjà intégré plusieurs nouvelles fonctionnalités et idées dans le monde du développement Web, telles que les files d'attente de tâches, l'authentification API prête à l'emploi, la communication en temps réel, et bien plus encore.

Tutoriel API Laravel - Construire un service Web RESTful

Dans ce didacticiel, nous allons explorer les moyens de créer et de tester une API robuste à l'aide de Laravel avec authentification. Nous utiliserons Laravel 5.4, et tout le code est disponible pour référence sur GitHub.

API RESTful

Tout d'abord, nous devons comprendre ce qui est exactement considéré comme une API RESTful. REST signifie REpresentational State Transfer et est un style architectural pour la communication réseau entre les applications, qui repose sur un protocole sans état (généralement HTTP) pour l'interaction.

Les verbes HTTP représentent des actions

Dans les API RESTful, nous utilisons les verbes HTTP comme actions, et les points de terminaison sont les ressources sur lesquelles agir. Nous utiliserons les verbes HTTP pour leur signification sémantique :

  • GET : récupérer des ressources
  • POST : créer des ressources
  • PUT : mettre à jour les ressources
  • DELETE : supprimer des ressources

Verbes HTTP : GET, POST, PUT et DELETE sont des actions dans les API RESTful

Action de mise à jour : PUT contre POST

Les API RESTful font l'objet de nombreux débats et il existe de nombreuses opinions sur la question de savoir s'il est préférable de mettre à jour avec POST , PATCH ou PUT , ou s'il est préférable de laisser l'action de création au verbe PUT . Dans cet article, nous utiliserons PUT pour l'action de mise à jour, car selon la RFC HTTP, PUT signifie créer/mettre à jour une ressource à un emplacement spécifique. Une autre exigence pour le verbe PUT est l'idempotence, ce qui dans ce cas signifie essentiellement que vous pouvez envoyer cette requête 1, 2 ou 1000 fois et le résultat sera le même : une ressource mise à jour dans la base de données.

Ressources

Les ressources seront les cibles des actions, dans notre cas les Articles et les Utilisateurs, et elles ont leurs propres endpoints :

  • /articles
  • /users

Dans ce didacticiel sur l'api laravel, les ressources auront une représentation 1: 1 sur nos modèles de données, mais ce n'est pas une exigence. Vous pouvez avoir des ressources représentées dans plusieurs modèles de données (ou pas du tout représentées dans la base de données) et des modèles complètement hors limites pour l'utilisateur. En fin de compte, vous décidez comment concevoir les ressources et les modèles d'une manière adaptée à votre application.

Une note sur la cohérence

Le plus grand avantage de l'utilisation d'un ensemble de conventions telles que REST est que votre API sera beaucoup plus facile à utiliser et à développer. Certains points de terminaison sont assez simples et, par conséquent, votre API sera beaucoup plus facile à utiliser et à entretenir au lieu d'avoir des points de terminaison tels que GET /get_article?id_article=12 et POST /delete_article?number=40 . J'ai construit des API terribles comme ça dans le passé et je me déteste toujours pour ça.

Cependant, il y aura des cas où il sera difficile de mapper vers un schéma de création/récupération/mise à jour/suppression. N'oubliez pas que les URL ne doivent pas contenir de verbes et que les ressources ne sont pas nécessairement des lignes dans une table. Une autre chose à garder à l'esprit est que vous n'êtes pas obligé d'implémenter chaque action pour chaque ressource.

Configuration d'un projet de service Web Laravel

Comme avec tous les frameworks PHP modernes, nous aurons besoin de Composer pour installer et gérer nos dépendances. Après avoir suivi les instructions de téléchargement (et ajouté à votre variable d'environnement de chemin), installez Laravel à l'aide de la commande :

 $ composer global require laravel/installer

Une fois l'installation terminée, vous pouvez échafauder une nouvelle application comme celle-ci :

 $ laravel new myapp

Pour la commande ci-dessus, vous devez avoir ~/composer/vendor/bin dans votre $PATH . Si vous ne voulez pas vous en occuper, vous pouvez également créer un nouveau projet à l'aide de Composer :

 $ composer create-project --prefer-dist laravel/laravel myapp

Avec Laravel installé, vous devriez pouvoir démarrer le serveur et tester si tout fonctionne :

 $ php artisan serve Laravel development server started: <http://127.0.0.1:8000> 

Lorsque vous ouvrez localhost:8000 sur votre navigateur, vous devriez voir la page d'exemple Laravel

Lorsque vous ouvrez localhost:8000 sur votre navigateur, vous devriez voir cet exemple de page.

Migrations et modèles

Avant d'écrire votre première migration, assurez-vous d'avoir créé une base de données pour cette application et ajoutez ses informations d'identification au fichier .env situé à la racine du projet.

 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret

Vous pouvez également utiliser Homestead, une boîte Vagrant spécialement conçue pour Laravel, mais cela sort un peu du cadre de cet article. Si vous souhaitez en savoir plus, reportez-vous à la documentation de Homestead.

Commençons par notre premier modèle et migration : l'article. L'article doit avoir un titre et un champ corps, ainsi qu'une date de création. Laravel fournit plusieurs commandes via Artisan, l'outil de ligne de commande de Laravel, qui nous aident en générant des fichiers et en les plaçant dans les bons dossiers. Pour créer le modèle Article, nous pouvons exécuter :

 $ php artisan make:model Article -m

L'option -m est l'abréviation de --migration et indique à Artisan d'en créer un pour notre modèle. Voici la migration générée :

 <?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateArticlesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('articles'); } }

Disséquons cela une seconde :

  • Les méthodes up() et down() seront exécutées respectivement lors de la migration et de la restauration ;
  • $table->increments('id') configure un entier à incrémentation automatique avec le nom id ;
  • $table->timestamps() configurera les horodatages pour nous — created_at et updated_at , mais ne vous souciez pas de définir une valeur par défaut, Laravel s'occupe de mettre à jour ces champs si nécessaire.
  • Et enfin, Schema::dropIfExists() supprimera bien sûr la table si elle existe.

Avec cela à l'écart, ajoutons deux lignes à notre méthode up() :

 public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('body'); $table->timestamps(); }); }

La méthode string() crée une colonne équivalente VARCHAR tandis que text() crée un équivalent TEXT . Cela fait, allons-y et migrons :

 $ php artisan migrate

Vous pouvez également utiliser l'option --step ici, et elle séparera chaque migration dans son propre lot afin que vous puissiez les annuler individuellement si nécessaire.

Laravel prêt à l'emploi est livré avec deux migrations, create_users_table et create_password_resets_table . Nous n'utiliserons pas la table password_resets , mais avoir la table des users prête pour nous sera utile.

Revenons maintenant à notre modèle et ajoutons ces attributs au champ $fillable afin que nous puissions les utiliser dans nos modèles Article::create et Article::update :

 class Article extends Model { protected $fillable = ['title', 'body']; }

Les champs à l'intérieur de la propriété $fillable peuvent être assignés en masse à l'aide des méthodes create() et update() d'Eloquent. Vous pouvez également utiliser la propriété $guarded pour autoriser toutes les propriétés sauf quelques-unes.

Amorçage de la base de données

L'ensemencement de la base de données est le processus de remplissage de notre base de données avec des données factices que nous pouvons utiliser pour la tester. Laravel est livré avec Faker, une excellente bibliothèque pour générer le format correct de données factices pour nous. Créons donc notre premier seeder :

 $ php artisan make:seeder ArticlesTableSeeder

Les seeders seront situés dans le répertoire /database/seeds . Voici à quoi cela ressemble après l'avoir configuré pour créer quelques articles :

 class ArticlesTableSeeder extends Seeder { public function run() { // Let's truncate our existing records to start from scratch. Article::truncate(); $faker = \Faker\Factory::create(); // And now, let's create a few articles in our database: for ($i = 0; $i < 50; $i++) { Article::create([ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]); } } }

Exécutons donc la commande seed :

 $ php artisan db:seed --class=ArticlesTableSeeder

Répétons le processus pour créer un générateur d'utilisateurs :

 class UsersTableSeeder extends Seeder { public function run() { // Let's clear the users table first User::truncate(); $faker = \Faker\Factory::create(); // Let's make sure everyone has the same password and // let's hash it before the loop, or else our seeder // will be too slow. $password = Hash::make('toptal'); User::create([ 'name' => 'Administrator', 'email' => '[email protected]', 'password' => $password, ]); // And now let's generate a few dozen users for our app: for ($i = 0; $i < 10; $i++) { User::create([ 'name' => $faker->name, 'email' => $faker->email, 'password' => $password, ]); } } }

Nous pouvons vous faciliter la tâche en ajoutant nos seeders à la classe DatabaseSeeder principale dans le dossier database/seeds :

 class DatabaseSeeder extends Seeder { public function run() { $this->call(ArticlesTableSeeder::class); $this->call(UsersTableSeeder::class); } }

De cette façon, nous pouvons simplement exécuter $ php artisan db:seed et il exécutera toutes les classes appelées dans la méthode run() .

Routes et contrôleurs

Créons les points de terminaison de base pour notre application : créer, récupérer la liste, en récupérer un seul, mettre à jour et supprimer. Sur le fichier routes/api.php , on peut simplement faire ceci :

 Use App\Article; Route::get('articles', function() { // If the Content-Type and Accept headers are set to 'application/json', // this will return a JSON structure. This will be cleaned up later. return Article::all(); }); Route::get('articles/{id}', function($id) { return Article::find($id); }); Route::post('articles', function(Request $request) { return Article::create($request->all); }); Route::put('articles/{id}', function(Request $request, $id) { $article = Article::findOrFail($id); $article->update($request->all()); return $article; }); Route::delete('articles/{id}', function($id) { Article::find($id)->delete(); return 204; })

Les routes à l'intérieur api.php seront préfixées par /api/ et le middleware de limitation de l'API sera automatiquement appliqué à ces routes (si vous souhaitez supprimer le préfixe, vous pouvez modifier la classe RouteServiceProvider sur /app/Providers/RouteServiceProvider.php ).

Déplaçons maintenant ce code vers son propre Controller :

 $ php artisan make:controller ArticleController

ArticleController.php :

 use App\Article; class ArticleController extends Controller { public function index() { return Article::all(); } public function show($id) { return Article::find($id); } public function store(Request $request) { return Article::create($request->all()); } public function update(Request $request, $id) { $article = Article::findOrFail($id); $article->update($request->all()); return $article; } public function delete(Request $request, $id) { $article = Article::findOrFail($id); $article->delete(); return 204; } }

Le fichier routes/api.php :

 Route::get('articles', 'ArticleController@index'); Route::get('articles/{id}', 'ArticleController@show'); Route::post('articles', 'ArticleController@store'); Route::put('articles/{id}', 'ArticleController@update'); Route::delete('articles/{id}', 'ArticleController@delete');

Nous pouvons améliorer les points de terminaison en utilisant la liaison de modèle de route implicite. De cette façon, Laravel injectera l'instance Article dans nos méthodes et renverra automatiquement un 404 s'il n'est pas trouvé. Il va falloir faire des changements sur le fichier routes et sur le contrôleur :

 Route::get('articles', 'ArticleController@index'); Route::get('articles/{article}', 'ArticleController@show'); Route::post('articles', 'ArticleController@store'); Route::put('articles/{article}', 'ArticleController@update'); Route::delete('articles/{article}', 'ArticleController@delete');
 class ArticleController extends Controller { public function index() { return Article::all(); } public function show(Article $article) { return $article; } public function store(Request $request) { $article = Article::create($request->all()); return response()->json($article, 201); } public function update(Request $request, Article $article) { $article->update($request->all()); return response()->json($article, 200); } public function delete(Article $article) { $article->delete(); return response()->json(null, 204); } }

Remarque sur les codes d'état HTTP et le format de réponse

Nous avons également ajouté l'appel response()->json() à nos points de terminaison. Cela nous permet de renvoyer explicitement des données JSON ainsi que d'envoyer un code HTTP qui peut être analysé par le client. Les codes les plus courants que vous renverrez seront :

  • 200 : OK. Le code de réussite standard et l'option par défaut.
  • 201 : Objet créé. Utile pour les actions en store .
  • 204 : Pas de contenu. Lorsqu'une action a été exécutée avec succès, mais qu'il n'y a pas de contenu à renvoyer.
  • 206 : Contenu partiel. Utile lorsque vous devez renvoyer une liste paginée de ressources.
  • 400 : Mauvaise requête. L'option standard pour les demandes qui échouent à la validation.
  • 401 : Non autorisé. L'utilisateur doit être authentifié.
  • 403 : Interdit. L'utilisateur est authentifié, mais n'a pas les autorisations pour effectuer une action.
  • 404 : Non trouvé. Ceci sera retourné automatiquement par Laravel lorsque la ressource n'est pas trouvée.
  • 500 : erreur interne du serveur. Idéalement, vous n'allez pas renvoyer explicitement ceci, mais si quelque chose d'inattendu se produit, c'est ce que votre utilisateur recevra.
  • 503 : Service indisponible. Assez explicite, mais aussi un autre code qui ne sera pas retourné explicitement par l'application.

Envoi d'une réponse 404 correcte

Si vous avez essayé de récupérer une ressource inexistante, vous recevrez une exception et vous recevrez tout le stacktrace, comme ceci :

NotFoundHttpException Stacktrace

Nous pouvons résoudre ce problème en modifiant notre classe de gestionnaire d'exceptions, située dans app/Exceptions/Handler.php , pour renvoyer une réponse JSON :

 public function render($request, Exception $exception) { // This will replace our 404 response with // a JSON response. if ($exception instanceof ModelNotFoundException) { return response()->json([ 'error' => 'Resource not found' ], 404); } return parent::render($request, $exception); }

Voici un exemple de retour :

 { data: "Resource not found" }

Si vous utilisez Laravel pour servir d'autres pages, vous devez modifier le code pour qu'il fonctionne avec l'en-tête Accept , sinon les erreurs 404 des requêtes régulières renverront également un JSON.

 public function render($request, Exception $exception) { // This will replace our 404 response with // a JSON response. if ($exception instanceof ModelNotFoundException && $request->wantsJson()) { return response()->json([ 'data' => 'Resource not found' ], 404); } return parent::render($request, $exception); }

Dans ce cas, les requêtes API auront besoin de l'en-tête Accept: application/json .

Authentification

Il existe de nombreuses façons d'implémenter l'authentification API dans Laravel (l'une d'entre elles étant Passport, un excellent moyen d'implémenter OAuth2), mais dans cet article, nous adopterons une approche très simplifiée.

Pour commencer, nous devrons ajouter un champ api_token à la table des users :

 $ php artisan make:migration --table=users adds_api_token_to_users_table

Et puis implémentez la migration :

 public function up() { Schema::table('users', function (Blueprint $table) { $table->string('api_token', 60)->unique()->nullable(); }); } public function down() { Schema::table('users', function (Blueprint $table) { $table->dropColumn(['api_token']); }); }

Après cela, exécutez simplement la migration en utilisant :

 $ php artisan migrate

Création du point de terminaison de registre

Nous utiliserons le RegisterController (dans le dossier Auth ) pour renvoyer la réponse correcte lors de l'enregistrement. Laravel est livré avec une authentification prête à l'emploi, mais nous devons encore l'ajuster un peu pour renvoyer la réponse souhaitée.

Si les API étaient en anglais, voici à quoi ressemblerait une conversation d'authentification API

Le contrôleur utilise le trait RegistersUsers pour implémenter l'enregistrement. Voici comment cela fonctionne:

 public function register(Request $request) { // Here the request is validated. The validator method is located // inside the RegisterController, and makes sure the name, email // password and password_confirmation fields are required. $this->validator($request->all())->validate(); // A Registered event is created and will trigger any relevant // observers, such as sending a confirmation email or any // code that needs to be run as soon as the user is created. event(new Registered($user = $this->create($request->all()))); // After the user is created, he's logged in. $this->guard()->login($user); // And finally this is the hook that we want. If there is no // registered() method or it returns null, redirect him to // some other URL. In our case, we just need to implement // that method to return the correct response. return $this->registered($request, $user) ?: redirect($this->redirectPath()); }

Nous avons juste besoin d'implémenter la méthode registered() dans notre RegisterController . La méthode reçoit le $request et le $user , donc c'est vraiment tout ce que nous voulons. Voici à quoi la méthode devrait ressembler à l'intérieur du contrôleur :

 protected function registered(Request $request, $user) { $user->generateToken(); return response()->json(['data' => $user->toArray()], 201); }

Et on peut le lier sur le fichier routes :

 Route::post('register', 'Auth\RegisterController@register');

Dans la section ci-dessus, nous avons utilisé une méthode sur le modèle User pour générer le jeton. Ceci est utile pour que nous n'ayons qu'un seul moyen de générer les jetons. Ajoutez la méthode suivante à votre modèle User :

 class User extends Authenticatable { ... public function generateToken() { $this->api_token = str_random(60); $this->save(); return $this->api_token; } }

Et c'est tout. L'utilisateur est maintenant enregistré et grâce à la validation de Laravel et à l'authentification prête à l'emploi, les champs name , email , password et password_confirmation sont obligatoires et les commentaires sont gérés automatiquement. Découvrez la méthode validator() dans le RegisterController pour voir comment les règles sont implémentées.

Voici ce que nous obtenons lorsque nous atteignons ce point de terminaison :

 $ curl -X POST http://localhost:8000/api/register \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name": "John", "email": "[email protected]", "password": "toptal123", "password_confirmation": "toptal123"}'
 { "data": { "api_token":"0syHnl0Y9jOIfszq11EC2CBQwCfObmvscrZYo5o2ilZPnohvndH797nDNyAT", "created_at": "2017-06-20 21:17:15", "email": "[email protected]", "id": 51, "name": "John", "updated_at": "2017-06-20 21:17:15" } }

Création d'un point de terminaison de connexion

Tout comme le point de terminaison d'enregistrement, nous pouvons modifier le LoginController (dans le dossier Auth ) pour prendre en charge notre authentification API. La méthode de login du trait AuthenticatesUsers peut être remplacée pour prendre en charge notre API :

 public function login(Request $request) { $this->validateLogin($request); if ($this->attemptLogin($request)) { $user = $this->guard()->user(); $user->generateToken(); return response()->json([ 'data' => $user->toArray(), ]); } return $this->sendFailedLoginResponse($request); }

Et on peut le lier sur le fichier routes :

 Route::post('login', 'Auth\LoginController@login');

Maintenant, en supposant que les seeders ont été exécutés, voici ce que nous obtenons lorsque nous envoyons une requête POST à ​​cette route :

 $ curl -X POST localhost:8000/api/login \ -H "Accept: application/json" \ -H "Content-type: application/json" \ -d "{\"email\": \"[email protected]\", \"password\": \"toptal\" }"
 { "data": { "id":1, "name":"Administrator", "email":"[email protected]", "created_at":"2017-04-25 01:05:34", "updated_at":"2017-04-25 02:50:40", "api_token":"Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw" } }

Pour envoyer le jeton dans une requête, vous pouvez le faire en envoyant un attribut api_token dans la charge utile ou en tant que jeton du porteur dans les en-têtes de la requête sous la forme d' Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw .

Déconnecter

Avec notre stratégie actuelle, si le jeton est erroné ou manquant, l'utilisateur devrait recevoir une réponse non authentifiée (que nous mettrons en œuvre dans la section suivante). Donc, pour un point de terminaison de déconnexion simple, nous enverrons le jeton et il sera supprimé de la base de données.

routes/api.php :

 Route::post('logout', 'Auth\LoginController@logout');

Auth\LoginController.php :

 public function logout(Request $request) { $user = Auth::guard('api')->user(); if ($user) { $user->api_token = null; $user->save(); } return response()->json(['data' => 'User logged out.'], 200); }

En utilisant cette stratégie, quel que soit le jeton dont dispose l'utilisateur, il sera invalide et l'API refusera l'accès (en utilisant des middlewares, comme expliqué dans la section suivante). Cela doit être coordonné avec le front-end pour éviter que l'utilisateur ne reste connecté sans avoir accès à aucun contenu.

Utilisation de middlewares pour restreindre l'accès

Avec l' api_token créé, nous pouvons basculer le middleware d'authentification dans le fichier routes :

 Route::middleware('auth:api') ->get('/user', function (Request $request) { return $request->user(); });

Nous pouvons accéder à l'utilisateur actuel en utilisant la méthode $request->user() ou via la façade Auth

 Auth::guard('api')->user(); // instance of the logged user Auth::guard('api')->check(); // if a user is authenticated Auth::guard('api')->id(); // the id of the authenticated user

Et on obtient un résultat comme celui-ci :

Une trace de pile InvalidArgumentException

En effet, nous devons modifier la méthode actuelle unauthenticated sur notre classe Handler. La version actuelle renvoie un JSON uniquement si la requête a l'en-tête Accept: application/json , alors changeons-le :

 protected function unauthenticated($request, AuthenticationException $exception) { return response()->json(['error' => 'Unauthenticated'], 401); }

Une fois cela corrigé, nous pouvons revenir aux points de terminaison de l'article pour les encapsuler dans le middleware auth:api . Nous pouvons le faire en utilisant des groupes de routes :

 Route::group(['middleware' => 'auth:api'], function() { Route::get('articles', 'ArticleController@index'); Route::get('articles/{article}', 'ArticleController@show'); Route::post('articles', 'ArticleController@store'); Route::put('articles/{article}', 'ArticleController@update'); Route::delete('articles/{article}', 'ArticleController@delete'); });

De cette façon, nous n'avons pas à définir le middleware pour chacune des routes. Cela ne fait pas gagner beaucoup de temps pour le moment, mais au fur et à mesure que le projet grandit, cela aide à garder les routes au SEC.

Tester nos terminaux

Laravel inclut l'intégration avec PHPUnit prête à l'emploi avec un phpunit.xml déjà configuré. Le framework nous fournit également plusieurs aides et assertions supplémentaires qui nous facilitent la vie, en particulier pour tester les API.

Il existe un certain nombre d'outils externes que vous pouvez utiliser pour tester votre API ; cependant, tester à l'intérieur de Laravel est une bien meilleure alternative - nous pouvons avoir tous les avantages de tester une structure d'API et des résultats tout en conservant le contrôle total de la base de données. Pour le point de terminaison de liste, par exemple, nous pourrions exécuter quelques usines et affirmer que la réponse contient ces ressources.

Pour commencer, nous devrons modifier quelques paramètres pour utiliser une base de données SQLite en mémoire. L'utilisation de cela rendra nos tests rapides comme l'éclair, mais le compromis est que certaines commandes de migration (contraintes, par exemple) ne fonctionneront pas correctement dans cette configuration particulière. Je vous conseille de vous éloigner de SQLite lors des tests lorsque vous commencez à avoir des erreurs de migration ou si vous préférez un ensemble de tests plus solides au lieu d'exécutions performantes.

Nous exécuterons également les migrations avant chaque test. Cette configuration nous permettra de construire la base de données pour chaque test puis de la détruire, en évitant tout type de dépendance entre les tests.

Dans notre fichier config/database.php , nous devrons configurer le champ de database de données dans la configuration sqlite sur :memory: :

 ... 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], ... ]

Activez ensuite SQLite dans phpunit.xml en ajoutant la variable d'environnement DB_CONNECTION :

 <php> <env name="APP_ENV" value="testing"/> <env name="CACHE_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/> <env name="QUEUE_DRIVER" value="sync"/> <env name="DB_CONNECTION" value="sqlite"/> </php>

Avec cela à l'écart, il ne reste plus qu'à configurer notre classe TestCase de base pour utiliser les migrations et amorcer la base de données avant chaque test. Pour ce faire, nous devons ajouter le trait DatabaseMigrations , puis ajouter un appel Artisan sur notre méthode setUp() . Voici la classe après les changements :

 use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Support\Facades\Artisan; abstract class TestCase extends BaseTestCase { use CreatesApplication, DatabaseMigrations; public function setUp() { parent::setUp(); Artisan::call('db:seed'); } }

Une dernière chose que j'aime faire est d'ajouter la commande test à composer.json :

 "scripts": { "test" : [ "vendor/bin/phpunit" ], ... },

La commande de test sera disponible comme ceci :

 $ composer test

Mise en place d'usines pour nos tests

Les usines nous permettront de créer rapidement des objets avec les bonnes données pour les tests. Ils se trouvent dans le dossier database/factories . Laravel est prêt à l'emploi avec une fabrique pour la classe User , ajoutons-en donc une pour la classe Article :

 $factory->define(App\Article::class, function (Faker\Generator $faker) { return [ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]; });

La bibliothèque Faker est déjà injectée pour nous aider à créer le bon format de données aléatoires pour nos modèles.

Nos premiers essais

Nous pouvons utiliser les méthodes assert de Laravel pour atteindre facilement un point de terminaison et évaluer sa réponse. Créons notre premier test, le test de connexion, à l'aide de la commande suivante :

 $ php artisan make:test Feature/LoginTest

Et voici notre essai :

 class LoginTest extends TestCase { public function testRequiresEmailAndLogin() { $this->json('POST', 'api/login') ->assertStatus(422) ->assertJson([ 'email' => ['The email field is required.'], 'password' => ['The password field is required.'], ]); } public function testUserLoginsSuccessfully() { $user = factory(User::class)->create([ 'email' => '[email protected]', 'password' => bcrypt('toptal123'), ]); $payload = ['email' => '[email protected]', 'password' => 'toptal123']; $this->json('POST', 'api/login', $payload) ->assertStatus(200) ->assertJsonStructure([ 'data' => [ 'id', 'name', 'email', 'created_at', 'updated_at', 'api_token', ], ]); } }

Ces méthodes testent quelques cas simples. La méthode json() atteint le point de terminaison et les autres assertions sont assez explicites. Un détail sur assertJson() : cette méthode convertit la réponse en un tableau recherche l'argument, donc l'ordre est important. Vous pouvez enchaîner plusieurs assertJson() dans ce cas.

Maintenant, créons le test de point de terminaison de registre et écrivons-en quelques-uns pour ce point de terminaison :

 $ php artisan make:test RegisterTest
 class RegisterTest extends TestCase { public function testsRegistersSuccessfully() { $payload = [ 'name' => 'John', 'email' => '[email protected]', 'password' => 'toptal123', 'password_confirmation' => 'toptal123', ]; $this->json('post', '/api/register', $payload) ->assertStatus(201) ->assertJsonStructure([ 'data' => [ 'id', 'name', 'email', 'created_at', 'updated_at', 'api_token', ], ]);; } public function testsRequiresPasswordEmailAndName() { $this->json('post', '/api/register') ->assertStatus(422) ->assertJson([ 'name' => ['The name field is required.'], 'email' => ['The email field is required.'], 'password' => ['The password field is required.'], ]); } public function testsRequirePasswordConfirmation() { $payload = [ 'name' => 'John', 'email' => '[email protected]', 'password' => 'toptal123', ]; $this->json('post', '/api/register', $payload) ->assertStatus(422) ->assertJson([ 'password' => ['The password confirmation does not match.'], ]); } }

Et enfin, le point de terminaison de déconnexion :

 $ php artisan make:test LogoutTest
 class LogoutTest extends TestCase { public function testUserIsLoggedOutProperly() { $user = factory(User::class)->create(['email' => '[email protected]']); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $this->json('get', '/api/articles', [], $headers)->assertStatus(200); $this->json('post', '/api/logout', [], $headers)->assertStatus(200); $user = User::find($user->id); $this->assertEquals(null, $user->api_token); } public function testUserWithNullToken() { // Simulating login $user = factory(User::class)->create(['email' => '[email protected]']); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; // Simulating logout $user->api_token = null; $user->save(); $this->json('get', '/api/articles', [], $headers)->assertStatus(401); } }

Il est important de noter que, lors des tests, l'application Laravel n'est pas réinstanciée sur une nouvelle requête. Ce qui signifie que lorsque nous atteignons le middleware d'authentification, il enregistre l'utilisateur actuel dans l'instance TokenGuard pour éviter d'accéder à nouveau à la base de données. Un choix judicieux, cependant - dans ce cas, cela signifie que nous devons diviser le test de déconnexion en deux, pour éviter tout problème avec l'utilisateur précédemment mis en cache.

Le test des points de terminaison Article est également simple :

 class ArticleTest extends TestCase { public function testsArticlesAreCreatedCorrectly() { $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $payload = [ 'title' => 'Lorem', 'body' => 'Ipsum', ]; $this->json('POST', '/api/articles', $payload, $headers) ->assertStatus(200) ->assertJson(['id' => 1, 'title' => 'Lorem', 'body' => 'Ipsum']); } public function testsArticlesAreUpdatedCorrectly() { $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $article = factory(Article::class)->create([ 'title' => 'First Article', 'body' => 'First Body', ]); $payload = [ 'title' => 'Lorem', 'body' => 'Ipsum', ]; $response = $this->json('PUT', '/api/articles/' . $article->id, $payload, $headers) ->assertStatus(200) ->assertJson([ 'id' => 1, 'title' => 'Lorem', 'body' => 'Ipsum' ]); } public function testsArtilcesAreDeletedCorrectly() { $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $article = factory(Article::class)->create([ 'title' => 'First Article', 'body' => 'First Body', ]); $this->json('DELETE', '/api/articles/' . $article->id, [], $headers) ->assertStatus(204); } public function testArticlesAreListedCorrectly() { factory(Article::class)->create([ 'title' => 'First Article', 'body' => 'First Body' ]); factory(Article::class)->create([ 'title' => 'Second Article', 'body' => 'Second Body' ]); $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $response = $this->json('GET', '/api/articles', [], $headers) ->assertStatus(200) ->assertJson([ [ 'title' => 'First Article', 'body' => 'First Body' ], [ 'title' => 'Second Article', 'body' => 'Second Body' ] ]) ->assertJsonStructure([ '*' => ['id', 'body', 'title', 'created_at', 'updated_at'], ]); } }

Prochaines étapes

C'est tout ce qu'on peut en dire. Il y a certainement place à amélioration - vous pouvez implémenter OAuth2 avec le package Passport, intégrer une couche de pagination et de transformation (je recommande Fractal), la liste est longue - mais je voulais passer en revue les bases de la création et du test d'une API dans Laravel sans forfaits externes.

Le développement de Laravel a certainement amélioré mon expérience avec PHP et la facilité de test avec lui a renforcé mon intérêt pour le framework. Ce n'est pas parfait, mais il est suffisamment flexible pour vous permettre de contourner ses problèmes.

Si vous concevez une API publique, consultez 5 règles d'or pour une excellente conception d'API Web.

En relation: Authentification complète de l'utilisateur et contrôle d'accès - Un didacticiel sur le passeport Laravel, Pt. 1