Tutorial API Laravel: come creare e testare un'API RESTful

Pubblicato: 2022-03-11

Con l'aumento dello sviluppo mobile e dei framework JavaScript, l'utilizzo di un'API RESTful è l'opzione migliore per creare un'unica interfaccia tra i tuoi dati e il tuo client.

Laravel è un framework PHP sviluppato pensando alla produttività degli sviluppatori PHP. Scritto e mantenuto da Taylor Otwell, il framework è molto supponente e si sforza di far risparmiare tempo agli sviluppatori favorendo le convenzioni rispetto alla configurazione. Il framework mira anche ad evolversi con il Web e ha già incorporato diverse nuove funzionalità e idee nel mondo dello sviluppo Web, come code di lavoro, autenticazione API pronta all'uso, comunicazione in tempo reale e molto altro.

Tutorial API Laravel - Creazione di un servizio Web RESTful

In questo tutorial esploreremo i modi in cui puoi creare e testare un'API robusta utilizzando Laravel con autenticazione. Useremo Laravel 5.4 e tutto il codice è disponibile come riferimento su GitHub.

API RESTful

Innanzitutto, dobbiamo capire cosa viene esattamente considerato un'API RESTful. REST sta per REpresentational State Transfer ed è uno stile architettonico per la comunicazione di rete tra applicazioni, che si basa su un protocollo stateless (solitamente HTTP) per l'interazione.

I verbi HTTP rappresentano le azioni

Nelle API RESTful, utilizziamo i verbi HTTP come azioni e gli endpoint sono le risorse su cui si agisce. Useremo i verbi HTTP per il loro significato semantico:

  • GET : recupera risorse
  • POST : crea risorse
  • PUT : aggiorna le risorse
  • DELETE : elimina le risorse

Verbi HTTP: GET, POST, PUT e DELETE sono azioni nelle API RESTful

Azione di aggiornamento: PUT e POST

Le API RESTful sono oggetto di molti dibattiti e ci sono molte opinioni là fuori se è meglio aggiornare con POST , PATCH o PUT o se è meglio lasciare l'azione di creazione al verbo PUT . In questo articolo utilizzeremo PUT per l'azione di aggiornamento, poiché secondo l'RFC HTTP, PUT significa creare/aggiornare una risorsa in una posizione specifica. Un altro requisito per il verbo PUT è l'idempotenza, che in questo caso significa sostanzialmente che puoi inviare quella richiesta 1, 2 o 1000 volte e il risultato sarà lo stesso: una risorsa aggiornata nel database.

Risorse

Le risorse saranno gli obiettivi delle azioni, nel nostro caso Articoli e Utenti, e avranno i propri endpoint:

  • /articles
  • /users

In questo tutorial di laravel API, le risorse avranno una rappresentazione 1:1 sui nostri modelli di dati, ma non è un requisito. È possibile avere risorse rappresentate in più di un modello di dati (o non rappresentate affatto nel database) e modelli completamente off limits per l'utente. Alla fine, puoi decidere come progettare risorse e modelli in modo che si adattino alla tua applicazione.

Una nota sulla coerenza

Il più grande vantaggio dell'utilizzo di una serie di convenzioni come REST è che la tua API sarà molto più facile da utilizzare e sviluppare. Alcuni endpoint sono piuttosto semplici e, di conseguenza, la tua API sarà molto più facile da usare e mantenere rispetto ad avere endpoint come GET /get_article?id_article=12 e POST /delete_article?number=40 . Ho creato API terribili come quella in passato e mi odio ancora per questo.

Tuttavia, ci saranno casi in cui sarà difficile eseguire il mapping a uno schema Crea/Recupera/Aggiorna/Elimina. Ricorda che gli URL non devono contenere verbi e che le risorse non sono necessariamente righe in una tabella. Un'altra cosa da tenere a mente è che non è necessario implementare ogni azione per ogni risorsa.

Configurazione di un progetto di servizio Web Laravel

Come con tutti i moderni framework PHP, avremo bisogno di Composer per installare e gestire le nostre dipendenze. Dopo aver seguito le istruzioni di download (e aver aggiunto al tuo percorso la variabile d'ambiente), installa Laravel usando il comando:

 $ composer global require laravel/installer

Al termine dell'installazione, puoi impalcare una nuova applicazione come questa:

 $ laravel new myapp

Per il comando precedente, devi avere ~/composer/vendor/bin nel tuo $PATH . Se non vuoi occupartene, puoi anche creare un nuovo progetto usando Composer:

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

Con Laravel installato, dovresti essere in grado di avviare il server e verificare se tutto funziona:

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

Quando apri localhost:8000 sul tuo browser, dovresti vedere la pagina di esempio di Laravel

Quando apri localhost:8000 sul tuo browser, dovresti vedere questa pagina di esempio.

Migrazioni e modelli

Prima di scrivere effettivamente la tua prima migrazione, assicurati di avere un database creato per questa app e aggiungi le sue credenziali al file .env che si trova nella radice del progetto.

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

Puoi anche usare Homestead, una scatola Vagrant appositamente realizzata per Laravel, ma questo è un po' fuori dallo scopo di questo articolo. Se desideri saperne di più, fai riferimento alla documentazione di Homestead.

Iniziamo con il nostro primo modello e la migrazione: l'articolo. L'articolo dovrebbe avere un titolo e un campo corpo, nonché una data di creazione. Laravel fornisce diversi comandi tramite Artisan, lo strumento da riga di comando di Laravel, che ci aiutano generando file e inserendoli nelle cartelle corrette. Per creare il modello Article, possiamo eseguire:

 $ php artisan make:model Article -m

L'opzione -m è l'abbreviazione di --migration e dice ad Artisan di crearne una per il nostro modello. Ecco la migrazione generata:

 <?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'); } }

Analizziamolo per un secondo:

  • I metodi up() e down() verranno eseguiti rispettivamente durante la migrazione e il rollback;
  • $table->increments('id') imposta un intero ad incremento automatico con il nome id ;
  • $table->timestamps() imposterà i timestamp per noi, created_at e updated_at , ma non preoccuparti di impostare un valore predefinito, Laravel si occupa di aggiornare questi campi quando necessario.
  • E infine, Schema::dropIfExists() , ovviamente, eliminerà la tabella se esiste.

Detto questo, aggiungiamo due righe al nostro metodo up() :

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

Il metodo string() crea una colonna equivalente VARCHAR mentre text() crea un equivalente TEXT . Fatto ciò, andiamo avanti e migrare:

 $ php artisan migrate

Puoi anche utilizzare l'opzione --step qui e separerà ogni migrazione nel proprio batch in modo da poterle ripristinare individualmente, se necessario.

Laravel out of the box viene fornito con due migrazioni, create_users_table e create_password_resets_table . Non useremo la tabella password_resets , ma avere la tabella users pronta per noi sarà utile.

Ora torniamo al nostro modello e aggiungiamo quegli attributi al campo $fillable in modo che possiamo usarli nei nostri modelli Article::create e Article::update :

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

I campi all'interno della proprietà $fillable possono essere assegnati in massa utilizzando i metodi create() e update() di Eloquent. Puoi anche utilizzare la proprietà $guarded , per consentire tutte le proprietà tranne alcune.

Inseminazione del database

Il seeding del database è il processo di riempimento del nostro database con dati fittizi che possiamo utilizzare per testarlo. Laravel viene fornito con Faker, un'ottima libreria per generare il formato corretto di dati fittizi per noi. Quindi creiamo il nostro primo seeder:

 $ php artisan make:seeder ArticlesTableSeeder

I seeder si troveranno nella directory /database/seeds . Ecco come appare dopo averlo impostato per creare alcuni articoli:

 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, ]); } } }

Quindi eseguiamo il comando seed:

 $ php artisan db:seed --class=ArticlesTableSeeder

Ripetiamo il processo per creare un seeder Utenti:

 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, ]); } } }

Possiamo renderlo più semplice aggiungendo i nostri seeders alla classe DatabaseSeeder principale all'interno della cartella database/seeds :

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

In questo modo, possiamo semplicemente eseguire $ php artisan db:seed ed eseguirà tutte le classi chiamate nel metodo run() .

Rotte e controllori

Creiamo gli endpoint di base per la nostra applicazione: creare, recuperare l'elenco, recuperarne uno singolo, aggiornare ed eliminare. Sul file routes/api.php , possiamo semplicemente fare questo:

 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; })

Le rotte all'interno api.php saranno precedute da /api/ e il middleware di limitazione dell'API verrà automaticamente applicato a queste rotte (se si desidera rimuovere il prefisso è possibile modificare la classe RouteServiceProvider su /app/Providers/RouteServiceProvider.php ).

Ora spostiamo questo codice sul proprio 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; } }

Il file 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');

Possiamo migliorare gli endpoint usando l'associazione del modello di percorso implicito. In questo modo, Laravel inietterà l'istanza Article nei nostri metodi e restituirà automaticamente un 404 se non viene trovato. Dovremo apportare modifiche al file dei percorsi e al controller:

 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); } }

Una nota sui codici di stato HTTP e sul formato di risposta

Abbiamo anche aggiunto la chiamata response()->json() ai nostri endpoint. Questo ci consente di restituire esplicitamente dati JSON e di inviare un codice HTTP che può essere analizzato dal client. I codici più comuni che restituirai saranno:

  • 200 : OK. Il codice di successo standard e l'opzione predefinita.
  • 201 : Oggetto creato. Utile per le azioni del store .
  • 204 : Nessun contenuto. Quando un'azione è stata eseguita correttamente, ma non è presente alcun contenuto da restituire.
  • 206 : Contenuto parziale. Utile quando devi restituire un elenco impaginato di risorse.
  • 400 : Cattiva richiesta. L'opzione standard per le richieste che non superano la convalida.
  • 401 : Non autorizzato. L'utente deve essere autenticato.
  • 403 : Proibito. L'utente è autenticato, ma non dispone delle autorizzazioni per eseguire un'azione.
  • 404 : Non trovato. Questo verrà restituito automaticamente da Laravel quando la risorsa non viene trovata.
  • 500 : Errore interno del server. Idealmente non lo restituirai esplicitamente, ma se qualcosa di inaspettato si interrompe, questo è ciò che il tuo utente riceverà.
  • 503 : Servizio non disponibile. Abbastanza autoesplicativo, ma anche un altro codice che non verrà restituito esplicitamente dall'applicazione.

Invio di una risposta 404 corretta

Se hai provato a recuperare una risorsa inesistente, ti verrà generata un'eccezione e riceverai l'intero stacktrace, in questo modo:

NotFoundHttpException Stacktrace

Possiamo risolverlo modificando la nostra classe di gestione delle eccezioni, situata in app/Exceptions/Handler.php , per restituire una risposta 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); }

Ecco un esempio di reso:

 { data: "Resource not found" }

Se stai usando Laravel per servire altre pagine, devi modificare il codice per lavorare con l'intestazione Accept , altrimenti 404 errori da richieste regolari restituiranno anche 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); }

In questo caso, le richieste API avranno bisogno dell'intestazione Accept: application/json .

Autenticazione

Esistono molti modi per implementare l'autenticazione API in Laravel (uno dei quali è Passport, un ottimo modo per implementare OAuth2), ma in questo articolo adotteremo un approccio molto semplificato.

Per iniziare, dovremo aggiungere un campo api_token alla tabella users :

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

E quindi implementare la migrazione:

 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']); }); }

Successivamente, esegui semplicemente la migrazione utilizzando:

 $ php artisan migrate

Creazione dell'endpoint del registro

Useremo RegisterController (nella cartella Auth ) per restituire la risposta corretta al momento della registrazione. Laravel viene fornito con l'autenticazione pronta all'uso, ma dobbiamo ancora modificarlo un po' per restituire la risposta che vogliamo.

Se le API fossero in inglese, ecco come suonerebbe una conversazione di autenticazione API

Il titolare del trattamento si avvale della caratteristica RegistersUsers per implementare la registrazione. Ecco come funziona:

 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()); }

Abbiamo solo bisogno di implementare il metodo registered() nel nostro RegisterController . Il metodo riceve $request e $user , quindi è davvero tutto ciò che vogliamo. Ecco come dovrebbe apparire il metodo all'interno del controller:

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

E possiamo collegarlo sul file dei percorsi:

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

Nella sezione precedente, abbiamo utilizzato un metodo sul modello User per generare il token. Questo è utile in modo che abbiamo un solo modo per generare i token. Aggiungi il seguente metodo al tuo modello utente:

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

E questo è tutto. L'utente è ora registrato e grazie alla convalida e all'autenticazione out of the box di Laravel, i campi name , email , password e password_confirmation sono obbligatori e il feedback viene gestito automaticamente. Verifica il metodo validator() all'interno di RegisterController per vedere come vengono implementate le regole.

Ecco cosa otteniamo quando raggiungiamo quell'endpoint:

 $ 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" } }

Creazione di un endpoint di accesso

Proprio come l'endpoint di registrazione, possiamo modificare LoginController (nella cartella Auth ) per supportare la nostra autenticazione API. Il metodo di login del tratto AuthenticatesUsers può essere sovrascritto per supportare la nostra 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); }

E possiamo collegarlo sul file dei percorsi:

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

Ora, supponendo che i seeders siano stati eseguiti, ecco cosa otteniamo quando inviamo una richiesta POST a quella 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" } }

Per inviare il token in una richiesta, puoi farlo inviando un attributo api_token nel payload o come token al portatore nelle intestazioni della richiesta sotto forma di Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw .

Disconnessione

Con la nostra strategia attuale, se il token è sbagliato o mancante, l'utente dovrebbe ricevere una risposta non autenticata (che implementeremo nella prossima sezione). Quindi, per un semplice endpoint di logout, invieremo il token e verrà rimosso dal database.

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); }

Usando questa strategia, qualunque token l'utente abbia non sarà valido e l'API negherà l'accesso (usando il middleware, come spiegato nella sezione successiva). Questo deve essere coordinato con il front-end per evitare che l'utente rimanga connesso senza avere accesso ad alcun contenuto.

Utilizzo di middleware per limitare l'accesso

Con l' api_token creato, possiamo attivare il middleware di autenticazione nel file delle rotte:

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

Possiamo accedere all'utente corrente utilizzando il metodo $request->user() o tramite la facciata 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

E otteniamo un risultato come questo:

Un InvalidArgumentException Stacktrace

Questo perché abbiamo bisogno di modificare l'attuale metodo non unauthenticated sulla nostra classe Handler. La versione corrente restituisce un JSON solo se la richiesta ha l'intestazione Accept: application/json , quindi cambiamola:

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

Con ciò risolto, possiamo tornare agli endpoint dell'articolo per avvolgerli nel middleware auth:api . Possiamo farlo utilizzando i gruppi di percorsi:

 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'); });

In questo modo non è necessario impostare il middleware per ciascuna delle route. Non fa risparmiare molto tempo in questo momento, ma man mano che il progetto cresce aiuta a mantenere le vie ASCIUTTE.

Testare i nostri endpoint

Laravel include l'integrazione con PHPUnit pronta all'uso con un phpunit.xml già impostato. Il framework ci fornisce anche diversi helper e asserzioni extra che ci rendono la vita molto più facile, specialmente per testare le API.

Esistono numerosi strumenti esterni che puoi utilizzare per testare la tua API; tuttavia, il test all'interno di Laravel è un'alternativa molto migliore: possiamo avere tutti i vantaggi del test di una struttura e dei risultati dell'API mantenendo il pieno controllo del database. Per l'endpoint dell'elenco, ad esempio, potremmo eseguire un paio di factory e affermare che la risposta contiene tali risorse.

Per iniziare, dovremo modificare alcune impostazioni per utilizzare un database SQLite in memoria. Il suo utilizzo renderà i nostri test velocissimi, ma il compromesso è che alcuni comandi di migrazione (vincoli, ad esempio) non funzioneranno correttamente in quella particolare configurazione. Ti consiglio di allontanarti da SQLite nei test quando inizi a ricevere errori di migrazione o se preferisci un set di test più forte invece di esecuzioni performanti.

Eseguiremo anche le migrazioni prima di ogni test. Questa configurazione ci consentirà di costruire il database per ogni test e quindi distruggerlo, evitando qualsiasi tipo di dipendenza tra i test.

Nel nostro file config/database.php , dovremo impostare il campo del database nella configurazione di sqlite su :memory: :

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

Quindi abilita SQLite in phpunit.xml aggiungendo la variabile di ambiente 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>

Detto questo, non resta che configurare la nostra classe TestCase di base per utilizzare le migrazioni e il seeding del database prima di ogni test. Per fare ciò, dobbiamo aggiungere il tratto DatabaseMigrations , quindi aggiungere una chiamata Artisan sul nostro metodo setUp() . Ecco la classe dopo le modifiche:

 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'); } }

Un'ultima cosa che mi piace fare è aggiungere il comando test a composer.json :

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

Il comando di test sarà disponibile in questo modo:

 $ composer test

Allestimento di fabbriche per i nostri test

Le fabbriche ci permetteranno di creare rapidamente oggetti con i dati giusti per i test. Si trovano nella cartella database/factories . Laravel esce dagli schemi con una factory per la classe User , quindi aggiungiamone una per la classe Article :

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

La libreria Faker è già stata iniettata per aiutarci a creare il formato corretto di dati casuali per i nostri modelli.

I nostri primi test

Possiamo usare i metodi assert di Laravel per raggiungere facilmente un endpoint e valutarne la risposta. Creiamo il nostro primo test, il login test, usando il seguente comando:

 $ php artisan make:test Feature/LoginTest

Ed ecco il nostro test:

 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', ], ]); } }

Questi metodi testano un paio di casi semplici. Il metodo json() raggiunge l'endpoint e le altre asserzioni sono piuttosto autoesplicative. Un dettaglio su assertJson() : questo metodo converte la risposta in un array cerca l'argomento, quindi l'ordine è importante. In questo caso puoi concatenare più chiamate assertJson() .

Ora creiamo il test dell'endpoint del registro e scriviamo un paio per quell'endpoint:

 $ 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.'], ]); } }

E infine, l'endpoint di logout:

 $ 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); } }

È importante notare che, durante il test, l'applicazione Laravel non viene istanziata nuovamente su una nuova richiesta. Ciò significa che quando raggiungiamo il middleware di autenticazione, salva l'utente corrente all'interno dell'istanza TokenGuard per evitare di colpire nuovamente il database. Una scelta saggia, tuttavia, in questo caso significa che dobbiamo dividere il test di logout in due, per evitare problemi con l'utente precedentemente memorizzato nella cache.

Anche il test degli endpoint dell'articolo è semplice:

 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'], ]); } }

Prossimi passi

Questo è tutto ciò che c'è da fare. C'è sicuramente spazio per miglioramenti: puoi implementare OAuth2 con il pacchetto Passport, integrare un livello di impaginazione e trasformazione (consiglio Fractal), l'elenco potrebbe continuare, ma volevo esaminare le basi della creazione e del test di un'API in Laravel senza pacchetti esterni.

Lo sviluppo di Laravel ha sicuramente migliorato la mia esperienza con PHP e la facilità di test con esso ha consolidato il mio interesse per il framework. Non è perfetto, ma è abbastanza flessibile da permetterti di aggirare i suoi problemi.

Se stai progettando un'API pubblica, dai un'occhiata a 5 regole d'oro per un'ottima progettazione di API Web.

Correlati: Autenticazione utente completa e controllo degli accessi - Un tutorial sul passaporto Laravel, pt. 1