Laravel-API-Tutorial: Erstellen und Testen einer RESTful-API

Veröffentlicht: 2022-03-11

Mit dem Aufkommen der mobilen Entwicklung und JavaScript-Frameworks ist die Verwendung einer RESTful-API die beste Option, um eine einzige Schnittstelle zwischen Ihren Daten und Ihrem Client zu erstellen.

Laravel ist ein PHP-Framework, das mit Blick auf die Produktivität von PHP-Entwicklern entwickelt wurde. Das von Taylor Otwell geschriebene und gepflegte Framework ist sehr eigensinnig und bemüht sich, Entwicklerzeit zu sparen, indem es Konventionen der Konfiguration vorzieht. Das Framework zielt auch darauf ab, sich mit dem Web weiterzuentwickeln und hat bereits mehrere neue Funktionen und Ideen in die Welt der Webentwicklung integriert – wie z.

Laravel-API-Tutorial – Erstellen eines RESTful-Webdienstes

In diesem Tutorial untersuchen wir, wie Sie eine robuste API mit Laravel mit Authentifizierung erstellen und testen können. Wir verwenden Laravel 5.4 und der gesamte Code ist als Referenz auf GitHub verfügbar.

RESTful-APIs

Zunächst müssen wir verstehen, was genau als RESTful-API gilt. REST steht für REpresentational State Transfer und ist ein Architekturstil für die Netzwerkkommunikation zwischen Anwendungen, der für die Interaktion auf ein zustandsloses Protokoll (normalerweise HTTP) angewiesen ist.

HTTP-Verben repräsentieren Aktionen

In RESTful-APIs verwenden wir die HTTP-Verben als Aktionen, und die Endpunkte sind die Ressourcen, auf die gehandelt wird. Wir verwenden die HTTP-Verben für ihre semantische Bedeutung:

  • GET : Ressourcen abrufen
  • POST : Ressourcen erstellen
  • PUT : Ressourcen aktualisieren
  • DELETE : Ressourcen löschen

HTTP-Verben: GET, POST, PUT und DELETE sind Aktionen in RESTful-APIs

Aktion aktualisieren: PUT vs. POST

RESTful-APIs werden viel diskutiert und es gibt viele Meinungen darüber, ob es am besten ist, mit POST , PATCH oder PUT zu aktualisieren, oder ob die Erstellungsaktion am besten dem Verb PUT überlassen wird. In diesem Artikel verwenden wir PUT für die Update-Aktion, da PUT laut HTTP-RFC bedeutet, eine Ressource an einem bestimmten Ort zu erstellen/aktualisieren. Eine weitere Anforderung für das PUT Verb ist Idempotenz, was in diesem Fall im Grunde bedeutet, dass Sie diese Anfrage 1, 2 oder 1000 Mal senden können und das Ergebnis dasselbe ist: eine aktualisierte Ressource in der Datenbank.

Ressourcen

Ressourcen sind die Ziele der Aktionen, in unserem Fall Artikel und Benutzer, und sie haben ihre eigenen Endpunkte:

  • /articles
  • /users

In diesem Laravel-API-Tutorial haben die Ressourcen eine 1:1-Darstellung in unseren Datenmodellen, aber das ist keine Voraussetzung. Sie können Ressourcen in mehr als einem Datenmodell darstellen (oder überhaupt nicht in der Datenbank darstellen) und Modelle, die für den Benutzer völlig tabu sind. Am Ende können Sie entscheiden, wie Sie Ressourcen und Modelle so gestalten, dass sie zu Ihrer Anwendung passen.

Ein Hinweis zur Konsistenz

Der größte Vorteil der Verwendung einer Reihe von Konventionen wie REST besteht darin, dass Ihre API viel einfacher zu verwenden und zu entwickeln ist. Einige Endpunkte sind ziemlich einfach und daher wird Ihre API viel einfacher zu verwenden und zu warten sein, im Gegensatz zu Endpunkten wie GET /get_article?id_article=12 und POST /delete_article?number=40 . Ich habe in der Vergangenheit solche schrecklichen APIs gebaut und hasse mich immer noch dafür.

Es wird jedoch Fälle geben, in denen es schwierig ist, ein Create/Retrieve/Update/Delete-Schema zuzuordnen. Denken Sie daran, dass die URLs keine Verben enthalten sollten und dass Ressourcen nicht unbedingt Zeilen in einer Tabelle sein müssen. Beachten Sie auch, dass Sie nicht jede Aktion für jede Ressource implementieren müssen.

Einrichten eines Laravel-Webdienstprojekts

Wie bei allen modernen PHP-Frameworks benötigen wir Composer, um unsere Abhängigkeiten zu installieren und zu handhaben. Nachdem Sie die Download-Anweisungen befolgt (und zu Ihrer path-Umgebungsvariable hinzugefügt haben), installieren Sie Laravel mit dem folgenden Befehl:

 $ composer global require laravel/installer

Nach Abschluss der Installation können Sie eine neue Anwendung wie folgt erstellen:

 $ laravel new myapp

Für den obigen Befehl müssen Sie ~/composer/vendor/bin in Ihrem $PATH haben. Wenn Sie sich damit nicht beschäftigen möchten, können Sie auch mit Composer ein neues Projekt erstellen:

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

Wenn Laravel installiert ist, sollten Sie in der Lage sein, den Server zu starten und zu testen, ob alles funktioniert:

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

Wenn Sie localhost:8000 in Ihrem Browser öffnen, sollten Sie die Laravel-Beispielseite sehen

Wenn Sie localhost:8000 in Ihrem Browser öffnen, sollten Sie diese Beispielseite sehen.

Migrationen und Modelle

Stellen Sie vor dem eigentlichen Schreiben Ihrer ersten Migration sicher, dass Sie eine Datenbank für diese App erstellt haben, und fügen Sie deren Anmeldeinformationen zur .env -Datei hinzu, die sich im Stammverzeichnis des Projekts befindet.

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

Sie können auch Homestead verwenden, eine Vagrant-Box, die speziell für Laravel hergestellt wurde, aber das würde den Rahmen dieses Artikels etwas sprengen. Wenn Sie mehr wissen möchten, lesen Sie die Homestead-Dokumentation.

Beginnen wir mit unserem ersten Modell und der ersten Migration – dem Artikel. Der Artikel sollte einen Titel und ein Textfeld sowie ein Erstellungsdatum haben. Laravel stellt über Artisan – das Befehlszeilentool von Laravel – mehrere Befehle bereit, die uns dabei helfen, Dateien zu generieren und sie in den richtigen Ordnern abzulegen. Um das Artikelmodell zu erstellen, können wir Folgendes ausführen:

 $ php artisan make:model Article -m

Die Option -m ist die Abkürzung für --migration und weist Artisan an, eine für unser Modell zu erstellen. Hier ist die generierte Migration:

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

Lassen Sie uns das für eine Sekunde analysieren:

  • Die Methoden up() und down() werden beim Migrieren bzw. Rollback ausgeführt;
  • $table->increments('id') richtet eine automatisch inkrementierende Ganzzahl mit dem Namen id ein;
  • $table->timestamps() richtet die Zeitstempel für uns ein – created_at und updated_at , aber machen Sie sich keine Gedanken über das Festlegen eines Standardwerts, Laravel kümmert sich um die Aktualisierung dieser Felder, wenn dies erforderlich ist.
  • Und schließlich wird Schema::dropIfExists() natürlich die Tabelle löschen, falls sie existiert.

Damit das aus dem Weg geräumt ist, fügen wir unserer Methode up() zwei Zeilen hinzu:

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

Die string() Methode erstellt eine VARCHAR -Äquivalentspalte, während text() ein TEXT -Äquivalent erstellt. Lassen Sie uns damit fortfahren und migrieren:

 $ php artisan migrate

Sie können hier auch die Option --step verwenden, die jede Migration in einen eigenen Stapel aufteilt, sodass Sie sie bei Bedarf einzeln zurücksetzen können.

Laravel wird standardmäßig mit zwei Migrationen geliefert, create_users_table und create_password_resets_table . Wir werden die Tabelle password_resets nicht verwenden, aber es wird hilfreich sein, die Tabelle users für uns bereit zu haben.

Kehren wir nun zu unserem Modell zurück und fügen diese Attribute dem $fillable -Feld hinzu, damit wir sie in unseren Article::create und Article::update -Modellen verwenden können:

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

Felder innerhalb der Eigenschaft $fillable können mit den Methoden create() und update() von Eloquent massenweise zugewiesen werden. Sie können auch die Eigenschaft $guarded verwenden, um alle bis auf wenige Eigenschaften zuzulassen.

Datenbank-Seeding

Beim Datenbank-Seeding wird unsere Datenbank mit Dummy-Daten gefüllt, die wir zum Testen verwenden können. Laravel wird mit Faker geliefert, einer großartigen Bibliothek, um genau das richtige Format von Dummy-Daten für uns zu generieren. Erstellen wir also unseren ersten Seeder:

 $ php artisan make:seeder ArticlesTableSeeder

Die Seeder befinden sich im Verzeichnis /database/seeds . So sieht es aus, nachdem wir es eingerichtet haben, um ein paar Artikel zu erstellen:

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

Lassen Sie uns also den Seed-Befehl ausführen:

 $ php artisan db:seed --class=ArticlesTableSeeder

Lassen Sie uns den Vorgang wiederholen, um einen Benutzer-Seeder zu erstellen:

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

Wir können es einfacher machen, indem wir unsere Seeder zur Hauptklasse DatabaseSeeder im Ordner database/seeds hinzufügen:

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

Auf diese Weise können wir einfach $ php artisan db:seed ausführen und es werden alle aufgerufenen Klassen in der Methode run() .

Routen und Controller

Lassen Sie uns die grundlegenden Endpunkte für unsere Anwendung erstellen: erstellen, die Liste abrufen, eine einzelne abrufen, aktualisieren und löschen. In der Datei routes/api.php können wir das einfach tun:

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

Den Routen in api.php wird das Präfix /api/ vorangestellt, und die API-Drosselungsmiddleware wird automatisch auf diese Routen angewendet (wenn Sie das Präfix entfernen möchten, können Sie die Klasse RouteServiceProvider in /app/Providers/RouteServiceProvider.php ).

Verschieben wir diesen Code nun auf einen eigenen Controller:

 $ php artisan make:controller ArticleController

ArtikelController.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; } }

Die Datei 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');

Wir können die Endpunkte verbessern, indem wir die implizite Routenmodellbindung verwenden. Auf diese Weise fügt Laravel die Article -Instanz in unsere Methoden ein und gibt automatisch einen 404 zurück, wenn sie nicht gefunden wird. Wir müssen Änderungen an der Routendatei und am Controller vornehmen:

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

Ein Hinweis zu HTTP-Statuscodes und dem Antwortformat

Wir haben unseren Endpunkten auch den Aufruf response()->json() hinzugefügt. Dadurch können wir explizit JSON-Daten zurückgeben und einen HTTP-Code senden, der vom Client analysiert werden kann. Die häufigsten Codes, die Sie zurücksenden, sind:

  • 200 : OK. Der Standard-Erfolgscode und die Standardoption.
  • 201 : Objekt erstellt. Nützlich für die store Aktionen.
  • 204 : Kein Inhalt. Wenn eine Aktion erfolgreich ausgeführt wurde, aber kein Inhalt zurückzugeben ist.
  • 206 : Teilinhalt. Nützlich, wenn Sie eine paginierte Liste von Ressourcen zurückgeben müssen.
  • 400 : Fehlerhafte Anfrage. Die Standardoption für Anfragen, die die Validierung nicht bestehen.
  • 401 : Nicht autorisiert. Der Benutzer muss authentifiziert werden.
  • 403 : Verboten. Der Benutzer ist authentifiziert, hat jedoch keine Berechtigungen zum Ausführen einer Aktion.
  • 404 : Nicht gefunden. Dies wird automatisch von Laravel zurückgegeben, wenn die Ressource nicht gefunden wird.
  • 500 : Interner Serverfehler. Idealerweise werden Sie dies nicht explizit zurückgeben, aber wenn etwas Unerwartetes kaputt geht, wird Ihr Benutzer dies erhalten.
  • 503 : Dienst nicht verfügbar. Ziemlich selbsterklärend, aber auch ein weiterer Code, der nicht explizit von der Anwendung zurückgegeben wird.

Senden einer korrekten 404-Antwort

Wenn Sie versucht haben, eine nicht vorhandene Ressource abzurufen, wird eine Ausnahme ausgelöst und Sie erhalten den gesamten Stacktrace wie folgt:

NotFoundHttpException Stacktrace

Wir können das beheben, indem wir unsere Exception-Handler-Klasse bearbeiten, die sich in app/Exceptions/Handler.php befindet, um eine JSON-Antwort zurückzugeben:

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

Hier ist ein Beispiel für die Rückgabe:

 { data: "Resource not found" }

Wenn Sie Laravel verwenden, um andere Seiten bereitzustellen, müssen Sie den Code so bearbeiten, dass er mit dem Accept -Header funktioniert, andernfalls werden 404-Fehler von regulären Anfragen ebenfalls ein JSON zurückgeben.

 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 diesem Fall benötigen die API-Anforderungen den Header Accept: application/json .

Authentifizierung

Es gibt viele Möglichkeiten, die API-Authentifizierung in Laravel zu implementieren (eine davon ist Passport, eine großartige Möglichkeit, OAuth2 zu implementieren), aber in diesem Artikel werden wir einen sehr vereinfachten Ansatz verfolgen.

Um zu beginnen, müssen wir der users ein api_token -Feld hinzufügen:

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

Und dann die Migration implementieren:

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

Führen Sie danach einfach die Migration aus mit:

 $ php artisan migrate

Erstellen des Registrierungsendpunkts

Wir verwenden den RegisterController (im Auth Ordner), um bei der Registrierung die richtige Antwort zurückzugeben. Laravel wird standardmäßig mit Authentifizierung geliefert, aber wir müssen es noch ein wenig optimieren, um die gewünschte Antwort zurückzugeben.

Wenn APIs auf Englisch wären, würde sich ein API-Authentifizierungsgespräch so anhören

Der Controller verwendet die Eigenschaft RegistersUsers , um die Registrierung zu implementieren. So funktioniert das:

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

Wir müssen nur die registered() -Methode in unserem RegisterController implementieren. Die Methode empfängt die $request und die $user , das ist also wirklich alles, was wir wollen. So sollte die Methode im Controller aussehen:

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

Und wir können es in der Routendatei verlinken:

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

Im obigen Abschnitt haben wir eine Methode des Benutzermodells verwendet, um das Token zu generieren. Dies ist nützlich, damit wir nur eine einzige Möglichkeit haben, die Token zu generieren. Fügen Sie Ihrem Benutzermodell die folgende Methode hinzu:

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

Und das ist es. Der Benutzer ist jetzt registriert und dank Laravels Validierung und sofort einsatzbereiter Authentifizierung sind die Felder name , email , password und password_confirmation erforderlich, und das Feedback wird automatisch verarbeitet. Sehen Sie sich die Methode validator() im RegisterController an, um zu sehen, wie die Regeln implementiert werden.

Folgendes erhalten wir, wenn wir diesen Endpunkt erreichen:

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

Erstellen eines Anmeldeendpunkts

Genau wie der Registrierungsendpunkt können wir den LoginController (im Auth -Ordner) bearbeiten, um unsere API-Authentifizierung zu unterstützen. Die login der Eigenschaft AuthenticatesUsers kann überschrieben werden, um unsere API zu unterstützen:

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

Und wir können es in der Routendatei verlinken:

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

Unter der Annahme, dass die Seeder ausgeführt wurden, erhalten wir Folgendes, wenn wir eine POST -Anforderung an diese Route senden:

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

Um das Token in einer Anfrage zu senden, können Sie dies tun, indem Sie ein Attribut api_token in der Nutzlast oder als Bearer-Token in den Anfrage-Headern in Form von Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw .

Ausloggen

Bei unserer aktuellen Strategie sollte der Benutzer, wenn das Token falsch ist oder fehlt, eine nicht authentifizierte Antwort erhalten (die wir im nächsten Abschnitt implementieren). Für einen einfachen Abmeldeendpunkt senden wir also das Token ein und es wird aus der Datenbank entfernt.

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

Mit dieser Strategie wird das Token, das der Benutzer hat, ungültig und die API verweigert den Zugriff (unter Verwendung von Middleware, wie im nächsten Abschnitt erläutert). Dies muss mit dem Frontend abgestimmt werden, um zu vermeiden, dass der Benutzer angemeldet bleibt, ohne Zugriff auf Inhalte zu haben.

Verwenden von Middlewares zum Einschränken des Zugriffs

Mit dem erstellten api_token können wir die Authentifizierungs-Middleware in der Routendatei umschalten:

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

Wir können auf den aktuellen Benutzer mit der Methode $request->user() oder über die Auth-Fassade zugreifen

 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

Und wir bekommen ein Ergebnis wie dieses:

Ein InvalidArgumentException Stacktrace

Dies liegt daran, dass wir die aktuelle unauthenticated Methode in unserer Handler-Klasse bearbeiten müssen. Die aktuelle Version gibt nur dann einen JSON zurück, wenn die Anfrage den Header Accept: application/json hat, also ändern wir ihn:

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

Nachdem dies behoben wurde, können wir zu den Artikelendpunkten zurückkehren, um sie in die auth:api Middleware einzubinden. Wir können das tun, indem wir Routengruppen verwenden:

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

Auf diese Weise müssen wir die Middleware nicht für jede der Routen festlegen. Es spart im Moment nicht viel Zeit, aber wenn das Projekt wächst, hilft es, die Routen TROCKEN zu halten.

Testen unserer Endpunkte

Laravel enthält die Integration mit PHPUnit aus der Box mit einer bereits eingerichteten phpunit.xml . Das Framework bietet uns auch mehrere Helfer und zusätzliche Zusicherungen, die uns das Leben erheblich erleichtern, insbesondere beim Testen von APIs.

Es gibt eine Reihe externer Tools, mit denen Sie Ihre API testen können. Das Testen innerhalb von Laravel ist jedoch eine viel bessere Alternative – wir können alle Vorteile des Testens einer API-Struktur und der Ergebnisse nutzen, während wir die volle Kontrolle über die Datenbank behalten. Für den Listenendpunkt könnten wir beispielsweise ein paar Fabriken ausführen und behaupten, dass die Antwort diese Ressourcen enthält.

Um zu beginnen, müssen wir einige Einstellungen optimieren, um eine In-Memory-SQLite-Datenbank zu verwenden. Wenn Sie das verwenden, werden unsere Tests blitzschnell ausgeführt, aber der Nachteil ist, dass einige Migrationsbefehle (z. B. Einschränkungen) in diesem bestimmten Setup nicht richtig funktionieren. Ich empfehle, beim Testen von SQLite wegzugehen, wenn Sie beginnen, Migrationsfehler zu bekommen, oder wenn Sie eine stärkere Reihe von Tests anstelle von leistungsfähigen Läufen bevorzugen.

Wir führen die Migrationen auch vor jedem Test durch. Dieses Setup ermöglicht es uns, die Datenbank für jeden Test zu erstellen und sie dann zu zerstören, wodurch jede Art von Abhängigkeit zwischen Tests vermieden wird.

In unserer Datei config/database.php müssen wir das database in der sqlite -Konfiguration auf :memory: einrichten:

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

Aktivieren Sie dann SQLite in phpunit.xml , indem Sie die Umgebungsvariable 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>

Nachdem dies aus dem Weg geräumt ist, müssen wir nur noch unsere Testfall-Basisklasse TestCase , um Migrationen zu verwenden und die Datenbank vor jedem Test mit Seeding zu versehen. Dazu müssen wir die DatabaseMigrations -Eigenschaft hinzufügen und dann einen Artisan -Aufruf für unsere setUp() Methode hinzufügen. Hier ist die Klasse nach den Änderungen:

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

Eine letzte Sache, die ich gerne mache, ist das Hinzufügen des Testbefehls zu composer.json :

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

Der Testbefehl wird wie folgt verfügbar sein:

 $ composer test

Einrichten von Fabriken für unsere Tests

Fabriken ermöglichen es uns, schnell Objekte mit den richtigen Daten zum Testen zu erstellen. Sie befinden sich im Ordner database/factories . Laravel wird mit einer Factory für die User -Klasse geliefert, also fügen wir eine für die Article -Klasse hinzu:

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

Die Faker-Bibliothek ist bereits eingefügt, um uns dabei zu helfen, das richtige Format von Zufallsdaten für unsere Modelle zu erstellen.

Unsere ersten Tests

Wir können die Assert-Methoden von Laravel verwenden, um auf einfache Weise einen Endpunkt zu treffen und seine Antwort auszuwerten. Lassen Sie uns unseren ersten Test, den Anmeldetest, mit dem folgenden Befehl erstellen:

 $ php artisan make:test Feature/LoginTest

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

Diese Methoden testen einige einfache Fälle. Die Methode json() erreicht den Endpunkt und die anderen Asserts sind ziemlich selbsterklärend. Ein Detail zu assertJson() : Diese Methode wandelt die Antwort in ein Array um, das nach dem Argument sucht, daher ist die Reihenfolge wichtig. In diesem Fall können Sie mehrere Aufrufe von assertJson() .

Lassen Sie uns nun den Register-Endpunkt-Test erstellen und ein paar für diesen Endpunkt schreiben:

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

Und zuletzt der Abmeldeendpunkt:

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

Es ist wichtig zu beachten, dass die Laravel-Anwendung während des Tests bei einer neuen Anfrage nicht erneut instanziiert wird. Das bedeutet, dass beim Aufrufen der Authentifizierungs-Middleware der aktuelle Benutzer in der TokenGuard Instanz gespeichert wird, um einen erneuten Zugriff auf die Datenbank zu vermeiden. Eine kluge Wahl – in diesem Fall bedeutet dies, dass wir den Abmeldetest in zwei Teile aufteilen müssen, um Probleme mit dem zuvor zwischengespeicherten Benutzer zu vermeiden.

Das Testen der Artikel-Endpunkte ist ebenfalls einfach:

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

Nächste Schritte

Das ist alles dazu. Es gibt definitiv Raum für Verbesserungen – Sie können OAuth2 mit dem Passport-Paket implementieren, eine Paginierungs- und Transformationsschicht integrieren (ich empfehle Fractal), die Liste geht weiter – aber ich wollte die Grundlagen zum Erstellen und Testen einer API in Laravel mit nein durchgehen externe Pakete.

Die Laravel-Entwicklung hat meine Erfahrung mit PHP sicherlich verbessert, und die Einfachheit des Testens damit hat mein Interesse an dem Framework gefestigt. Es ist nicht perfekt, aber es ist flexibel genug, damit Sie seine Probleme umgehen können.

Wenn Sie eine öffentliche API entwerfen, sehen Sie sich die 5 goldenen Regeln für großartiges Web-API-Design an.

Siehe auch: Vollständige Benutzerauthentifizierung und Zugriffskontrolle – Ein Laravel-Passport-Tutorial, Pt. 1