Vor wenigen Jahren wurden Webapplikation noch so gebaut, dass die Interaktionen mit Datenbank und View unmittelbar von der Applikation selbst durchgeführt wurden. Doch mit der steigenden Popularität von mobilen Apps und Javascript-Frameworks, etablierten sich zentrale RESTful-APIs als die beste und eleganteste Lösung, wenn es darum geht, eine einheitliche Schnittstelle zwischen dem jeweiligen Client und dem Datenstamm bereitzustellen.
Bei Laravel handelt es sich um ein in PHP geschriebenes Open-Source-Framework, welches im Jahr 2011 von Taylor Otwell initiiert wurde. Inzwischen erfreut es sich aufgrund seiner Einfachheit und Flexibilität großer Beliebtheit und punktet mit einer weltweit verbreiteten Community.
Der Ausdruck „RESTful API“ ist vielen von euch sicherlich ein Begriff. Um darauf näher eingehen zu können, muss man erstmal verstehen, wofür „REST“ eigentlich steht, und welche Prinzipien es verfolgt.
REST ist die Abkürzung für „REpresentational State Transfer“, und bezeichnet ein Programmierparadigma zur Kommunikation zwischen Applikation über ein zustandsloses Protokoll (meistens HTTP). Besonders relevant ist dieses für verteilte Systeme, insbesondere für Webservices.
Bei REST-konformen APIs stellen die Ressourcen die Endpunkte dar. HTTP-Methoden repräsentieren die zugehörigen Actions. Es ist genau definiert, bei welcher Zugriffsart welche Aktion ausgelöst werden soll:
Die HTTP-Methoden HEAD, OPTIONS, CONNECT und TRACE sind optional, und werden für CRUD-Operationen im Normalfall nicht benötigt. Wichtig wäre es hier auch noch anzumerken, dass eine Eigenimplementierung von CONNECT oder TRACE sich ggf. auf die Sicherheit der Applikation auswirken kann!
Wenn es um die Speicherung von Daten geht, sei es das Neuanlegen oder auch Aktualisieren, teilen sich die Meinungen, ob man dafür nun lieber POST, PUT oder auch PATCH (partielles Update) verwenden sollte.
In den folgenden Beispielen wird für das Aktualisieren von Ressourcen auf PUT gesetzt. PUT steht für das Anlegen bzw. Aktualisieren einer Ressource an einem bestimmten Ort.
Was ebenfalls für PUT steht, ist dessen Idempotenz. Das bedeutet, dass wiederholte Anfragen mit den gleichen Daten tatsächlich nur eine einzige Änderung bewirken.
Ursprünglich sollte dieses Framework lediglich eine bessere Alternative zum bereits beliebten CodeIgniter-Framework darstellen, da dieses u.a. keine Funktionen betreffend Authentifizierung mitbrachte.
Mit der Zeit wurde dessen Funktionsumfang in großem Maße erweitert, und brachte neben der Unterstützung von Dependency Injections auch eine eigene Template-Engine („Blade“) mit.
Im Jahr 2012 erfolgte dann mit dem mitgelieferten Command-Line-Tool „Artisan“ ein weiterer großer Durchbruch, da dieses es ermöglicht, sämtliche Interaktionen mit dem Framework unmittelbar über die Konsole auszuführen. Des Weiteren folgte auch eine Unterstützung für eine noch breitere Palette an Datenbanksystemen.
Als das Laravel-Framework mit Version 4 dann vollständig über Composer geladen und verwaltet werden konnte, stieg auch dessen Erweiterbarkeit entsprechend an, was dem Entwickler noch mehr Freiheiten einräumte.
Weitere Informationen diesbezüglich können direkt auf der Homepage des Frameworks eingesehen werden.
Für die folgenden Beispiele gehen wir davon aus, dass das Laravel-Framework bereits installiert und lauffähig ist. Ob dies nun via Homestead, Composer oder auch einer manuellen Installation eines veröffentlichten Releases von GitHub erfolgt, spielt hier keine Rolle. Des Weiteren sollte auch die Datenbankverbindung bereits konfiguriert und funktionsfähig sein.
Wir treffen die Annahme, dass für das folgende Beispiel der Model-Name ident zum Ressourcen-Namen ist. Dies ist keine zwingend erforderliche Maßnahme, fördert jedoch die Nachvollziehbarkeit ungemein.
Als Ressource werden wir hier eine „Notiz“ (engl. „Note“) anlegen, welche einen Titel („Subject“) sowie einen Textkörper („Body“) hat.
Beginnen wir nun als Erstes damit, das Model inkl. Migration mit Hilfe des von Laravel mitgelieferten Command-Line-Tools „Artisan“ zu erstellen.
Dazu navigieren wir mit dem Terminal (Linux) bzw. der Eingabeaufforderung (Windows) direkt in das Projektverzeichnis und geben dabei den folgenden Command ein:
php artisan make:model Note -m
Mit diesem Command wird ein neues Model namens „Note“ erstellt. Der optionale Paramter „-m“ gibt an, dass parallel dazu eine entsprechende Migration angelegt wird. Das soeben generierte Model liegt unter ./app/Note.php, die zugehörige Migration unter ./database/migrations/YYYY_MM_DD_XXXXXX_create_notes_table.php.
Sehen wir uns nun zunächst mal die Migration an.
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateNotesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('notes', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('notes'); } }
Die up() und down()-Methoden dienen der Durchführung der Migration bzw. deren Rollback.
Mit $table->increments(‚id‘) wird festgelegt, dass hierfür ein INT-Feld mit Auto-Increment als Primary-Key erzeugt wird. Die Anweisung $table->timestamps() sorgt dafür, dass zusätzlich die von Laravel verwalteten Felder „created_at“ und „updated_at“ ebenfalls erzeugt werden. Ob diese nun genutzt werden sollen oder nicht, lässt sich übrigens auch direkt im Model konfigurieren, aber standardmäßig werden diese verwendet.
Fügen wir der zuvor generierten Migration nun unsere beiden Felder „Subject“ und „Body“ hinzu, sodass die up()-Methode dann schließlich wie folgt aussieht:
public function up() { Schema::create('notes', function (Blueprint $table) { $table->increments('id'); $table->string('subject'); $table->text('body'); $table->timestamps(); }); }
Führen wir nun die Migration mit dem folgenden Command aus:
php artisan migrate
Sollte es möglicherweise zu einem Fehler wie „Specified key was too long error“ kommen, könnte die folgende Maßnahme helfen.
Dazu erweitern wir den App-Service-Provider unter ./app/Providers/AppServiceProvider.php wie folgt und wiederholen anschließend den vorigen Schritt:
use Illuminate\Support\Facades\Schema; public function boot() { Schema::defaultStringLength(191); }
Widmen wir uns nun jedoch wieder unserem „Note“-Model. Diesem fügen wir nun das „protected“-Attribut „fillable“ hinzu, und legen fest, welche Felder davon in der Datenbank befüllt werden dürfen.
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Note extends Model { /** * @var array */ protected $fillable = ['subject', 'body']; }
Unter dem Begriff Datenbank-Seeding versteht man das Befüllen einer Datenbank bzw. einer Datenbank-Tabelle (bei relationalen Systemen) mit Testdaten. Diese werden dabei i.d.R. dynamisch nach dem Zufallsprinzip erzeugt.
Erstellen wir mit dem folgenden Command nun eine Seeder-Klasse, welche unsere Notes-Tabelle mit Testdaten befüllen wird:
php artisan make:seeder NotesSeeder
Nun öffnen wir die soeben erzeugte Seeder-Klasse unter ./database/seeds/NotesSeeder.php und fügen hier Folgendes ein:
<?php use Illuminate\Database\Seeder; use App\Note; class NotesSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { // truncate already existing data Note::truncate(); // make instance of the Faker-class $faker = \Faker\Factory::create(); // insert some random generated records for ($i = 0; $i < 200; $i++) { Note::create([ 'subject' => $faker->sentence(), 'body' => $faker->paragraph(), ]); } } }
Die Faker-Klasse ermöglicht die Generierung von zufälligen Pseudo-Inhalten, selektierbar nach Typ. Hier wird als erstes die Datenbank-Tabelle geleert, sodass wir mit jeder Ausführung dieses Seeders wieder von Neuem starten können. In einer Schleife werden nun 200 zufällige Einträge generiert und gespeichert. Um den Seeder nun auszuführen, machen wir uns wieder Artisan zunutze:
php artisan db:seed –class=NotesSeeder
Sollten wir mehrere Seeder-Klassen haben, wäre es einfacher, diese dem zentralen „Database-Seeder“ hinzuzufügen, indem wir die einzelnen, von uns erstellten Seeder-Klassen in der run()-Methode der DatabaseSeeder()-Klasse im gleichen Verzeichnis registrieren.
<?php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $this->call( NotesSeeder::class ); } }
Nun können wir mit dem Artisan-Seed-Command ohne spezifische Angabe einer Klasse alle im DatabaseSeeder() registrierten Seeder ausführen.
php artisan db:seed
Damit wir mit den Daten interagieren können, bedarf es eines Controllers, welcher die Kommunikation zwischen Client und Datenbank übernimmt. In diesem Fall benötigen wir jedoch einen sogenannten Resource-Controller, d.h. er muss mindestens die folgenden Actions implementieren: index(), store(), update() und destroy(). Dazu nutzen wir ebenfalls wieder Artisan:
php artisan make:controller NotesController
Der neu erstellte NotesController() befindet sich unter ./app/Http/Controllers/NotesController.php. In diesem legen wir nun die vorhin genannten Actions an, und implementieren das Basis-Verhalten.
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Note; class NotesController extends Controller { /** * Index function for general listing. * * @param Request $request * * @return \Illuminate\Http\JsonResponse */ public function index(Request $request) { $notes = Note::all(); return response()->json($notes); } /** * Store-Action * * @param Request $request * * @return \Illuminate\Http\JsonResponse */ public function store(Request $request) { $note = Note::create($request->all()); return response()->json($note); } /** * Show-Action * * @param Request $request * @param int $id * * @return \Illuminate\Http\JsonResponse */ public function show(Request $request, $id) { $note = Note::find($id); return response()->json($note); } /** * Update-Action * * @param Request $request * @param int $id * * @return \Illuminate\Http\JsonResponse */ public function update(Request $request, $id) { $note = Note::findOrFail($id); $note->update($request->all()); return response()->json($note); } /** * Destroy-Action * * @param Request $request * @param int $id * * @return \Illuminate\Http\JsonResponse */ public function destroy(Request $request, $id) { Note::find($id)->delete(); return response()->json([], 204); } }
Bei mehreren Resource-Controllern würde es Sinn machen, dafür einen Basis-Resource-Controller anzulegen, und die jeweiligen Resource-Controller davon erben zu lassen. Das hat den Vorteil, das Basisverhalten in jedem Controller nutzen zu können und bringt gleichzeitig auch die Möglichkeit, für einzelne Controller bestimmte Actions zu überschreiben.
Nachdem nun das CRUD-Verhalten grundsätzlich implementiert ist, müssen nur noch die Routen definiert werden. Dazu öffnen wir die Datei ./routes/api.php und fügen dort die folgende Zeile ein:
Route::resource('notes', 'NotesController');
Die Routen-Konfiguration von Laravel erlaubt es auf diese Art einen Endpunkt direkt auf einen Resource-Controller zu routen. Laravel hat hierfür bereits bestimmte Actions beim jeweiligen Controller für sich reserviert. Dabei handelt es sich um jene, die wir bereits zuvor beim NotesController() angelegt haben. Die folgende Tabelle soll Aufschluss geben wie die URL-Struktur für einen solchen Resource-Controller aussieht, und welche Action hinter welcher HTTP-Methode liegt.
HTTP-Verbindungsart | URI | Action | Routen-Name |
GET | /notes | index | notes.index |
GET | /notes/create | create | notes.create |
POST | /notes | store | notes.store |
GET | /notes/{id} | show | notes.show |
GET | /notes/{id}/edit | edit | notes.edit |
PUT/PATCH | /notes/{id} | update | notes.update |
DELETE | /notes/{id} | destroy | notes.destroy |
Zu beachten ist, dass in unserem Resource-Controller die Actions create() sowie edit() nicht implementiert wurden. Dies hat den Grund, da bei diesen beiden Actions nur das jeweilige (HTML-)Formular via GET ausgeliefert werden sollte. Nachdem aber unser Ressource-Endpunkt nur JSON-Daten ausliefert, wäre dies somit hinfällig.
Bei REST-APIs sind neben den HTTP-Methoden unter anderem auch die zu verwendenden HTTP-Statuscodes und deren Bedeutung festgelegt. Die folgende Auflistung soll hier einen kurzen Überblick gewähren.
Die Nutzung dieser Statuscodes ist zwar nicht verpflichtend, aber wenn man REST-konform entwickeln möchte, unabdinglich.
Gerade in Zeiten, wo Rich-Internet-Applications (RIA) bzw. Single-Page-Applications wie Angular oder auch React sich enormer Beliebtheit erfreuen, und auch Apps für mobile Geräte immer häufiger auf zentrale Datenstämme zugreifen müssen, wird es immer wichtiger, einheitliche Kommunikationskanäle zu verwenden.
Mit einer REST-konformen API (RESTful) lässt sich so etwas auch relativ einfach in der Praxis realisieren.
Was jedoch in diesen Beispielen der Einfachheit halber nicht berücksichtigt wurde, ist die Absicherung der API. Besonders relevant ist dieser Punkt dann, wenn Daten modifiziert werden können. Es gibt ein sehr großes Spektrum an möglichen Varianten – angefangen bei einfachen Tokens (beispielsweise JSON-Web-Token, JWT) bis hin zur komplexen OAuth-Authentifizierung.