Headless Blog mit dem Laravel Framework
Einleitung
Laravel ist ein sehr weitverbreitetes PHP-Framework und gehört zur den Kernwerkzeugen bei uns im WTL.
Im Zuge unseres Academy-Days habe ich Laravel eingesetzt, um einen HeadlessBlog zu implementieren. Dabei habe ich diese Implementierung als Referenzprojekt erstellt, um den Tag bestmöglich vorbereiten zu können.
Als vorbereitete Rahmenbedingung gab es eine spezifizierte API-Struktur und dazugehörige API-Tests.
Das übergeordnete Ziel unseres Projektes war festzustellen:
- Ist der HeadlessBlog innerhalb eines Tages umsetzbar?
- Genügt die Definition der API, um den Blog umsetzen zu können?
- Sind die API-Test ausreichend gut definiert, um sicherstellen zu können, dass die Projekte vergleichbar sind?
Da Laravel zu meinen Standard-Tools gehört, war es für mich persönlich besonders interessant folgende Aspekte zu betrachten:
- Wie schnell kann ich das Projekt umsetzen?
- Komme ich ohne Schwierigkeiten durch?
- Wie schneidet Laravel gegenüber den anderen Projekten ab?



Datenbank
Die Datenbank beinhaltet drei Tabellen, die folgendermaßen miteinander verknüpft sind:

Ein User kann der Author von einem oder mehreren Posts und/oder Kommentaren sein. Ein Post kann keinen oder mehrere Kommentare haben.
NGiNX
Der Nginx beinhaltet einen vhost, welcher auf Port 80 läuft.
Laravel
Das Setup von Laravel konnte mit der Standardausprägung der composer.json vom Framework erfolgen, da im Zuge dieses Projektes keine Besonderen Bibliotheken benötigt wurden.
Die Installation erfolgte dann via:
composer install
Docker
Das lokale Setup erfolgt mit Docker, welches gleichzeitig auch die Vorgabe im Rahmen des AcademyDays darstellte. Es gibt jeweils einen Container für PHP, Nginx und MySQL, welche via docker-compose organisiert und ausgeführt werden.
Gestartet wird die Umgebung via:
docker-compose -p hblog up --build
Datenbank Migrationen
Im Vorfeld der Implementierung habe ich zunächst Migrationen erstellt. Im Anschluss wurde von mir die BusinessLogik programmiert und mit der API-Definition abgeglichen. Migrationen werden in Laravel über eine Datenbanktabelle migrations organisiert. Sollte diese Tabelle beim Ausführen der Migrationen nicht existieren, wird diese automatisch angelegt. Das Erstellen der Migrationen erfolgt bei Laravel über ein Konsolenkommando:
php artisan make:migration AddTablePost
Es wird eine Datei im Migrationen-Ordner erstellt, die ich dann wie abgebildet angepasst habe. Mein Beispiel zeigt die Migration für die Tabelle posts mit den Spalten id, message, author_id, updated_at und created_at. Zusätzlich wird ein ForeignKey auf die users-Tabelle erzeugt. Im Falle eines Rollbacks wird die Tabelle posts gelöscht.
use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class AddTablePost extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->string('message'); $table->unsignedInteger('author_id'); $table->timestamps(); $table->foreign('author_id') ->references('id')->on('users')->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('posts'); } }
Das Rollback einer Migration erfolgt über:
php artisan migration rollback

Die Routen werden im Ordner routes in dem Fall in der Datei api.php erstellt. Dabei wird die Route definiert und auf die entsprechende Action im Controller verlinkt. Das folgende Beispiel zeigt die Definition der Routen für die Kommentare:
Route::group( [ 'prefix' => '{id}/comments' ], function($id) { Route::get('/', [ 'uses' => 'CommentController@index', ]); Route::post('/',[ 'middleware' => 'auth:api', 'uses' => 'CommentController@add', ]); Route::put('/{commentId}',[ 'middleware' => 'auth:api', 'uses' => 'CommentController@edit', ]); Route::get('/{commentId}', [ 'uses' => 'CommentController@get', ]); Route::delete('/{commentId}', [ 'middleware' => 'auth:api', 'uses' => 'CommentController@delete', ]); } );
Controller
Im folgendem Beispiel wird die Logik für das Anlegen eines Blog-Posts gezeigt. Dabei werden alle relevanten Daten, wie message und user aus dem Request gelesen. (Anmerkung: In einer perfekten Welt wäre dieser Code hier niemals im Controller selber gelandet, aber im Zuge dieses Referenzprojektes erschien mir das ausreichend.)
/** * @param Request $request * * @return \App\Http\Resources\Post */ public function add(AddPostRequest $request) { $post = new Post($request->all()); $post->author()->associate($request->user()); $post->save(); return new \App\Http\Resources\Post($post); }
Als erstes wird ein neuer Post mit den Daten des Requests angelegt. Im nächsten Schritt wird der angemeldete User (also der User dessen Token mitgegeben wurde) als Author des Posts gesetzt und anschließend wird der Post gespeichert.
Validierung
Um sicherzustellen, dass die Daten im Request im richtigen Format vorliegen, wird in meiner Implementierung die Laravel-Formrequest-Validierung verwendet. Dabei wird ein FormRequest erstellt, der bestimmte Validierungsregeln enthält, welche direkt bei der Erstellung des Requests ausgewertet werden. Im Falle eines Fehlers gibt die API einen 422-Response mit den entsprechenden Fehlermeldungen zurück.
namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class AddPostRequest extends FormRequest { public function rules () { return [ 'message' => 'required', ]; } }
Im folgenden Beispiel wird ein beispielhafter Response einer fehlgeschlagenen Validierung dargestellt.
{ "message": [ "The message field is required." ] }
namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; /** * Class Post * * @package App\Http\Resources */ class Post extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, 'message' => $this->message, 'author' => [ 'id' => $this->author->id, 'name' => $this->author->name, 'email' => $this->author->email, ], 'comments' => (new CommentCollection($this->comments))->collection, ]; } }
In dieser Resource wird festgelegt, dass ich die Felder id, message, author und comments eines Posts haben möchte. Vom Author benötige ich nur die Felder id, name und email. Die Struktur der Kommentare werden in einer eigenen Resource festgelegt. Der resultierende JSON-Response sieht wie folgt aus:
{ "id": 23, "message": "myHardCodedValue", "author": { "id": 34, "name": "myHardCodedValue", "email": "6hA0VfJmTB1Uvx4JeIC6@foo.com" }, "comments": [ { "id": 13, "message": "Testkommentar", "post_id": 23, "author": { "id": 34, "name": "myHardCodedValue", "email": "6hA0VfJmTB1Uvx4JeIC6@foo.com" } } ] }
Wie definiert erhalten wir als Response einen Post, mit Author und einer Liste an Kommentaren.
Ausführung
Ich habe den Blog soweit möglich mit Postman getestet. Dazu habe ich natürlich einige manuelle Requests geschickt und ausgewertet, als auch eine Testsuite erstellt und ausgeführt. Das war gleichzeitig die erste Version unserer API-Tests. In einer weiteren Iteration, haben wir Chakram als API-Test-Tool eingeführt. Zusätzlich habe ich ein paar Performance-Tests mit artillery.io und gatling.io ausgeführt.
Erkenntnisse und Verbesserungen
Vorallem im Zuge der Chakram-Tests und der Performance-Tests ist mir aufgefallen, dass meine JavaScript-Kollegen deutlich schneller unterwegs waren. Ich habe dann debugged und an den Einstellungen des PHP-FPM und NGINX herumgestellt. Leider ohne größere Erfolge. Als ich einen Profiler dazugenommen habe, ist mir aufgefallen, dass ich unglaublich viele Datenbankabfragen für meine 10 Blogposts durchgeführt habe. Ich konnte dann tatsächlich Lazy-Loading als den bösen Buben identifizieren und habe auf Eager-Loading umgestellt. Das folgende Beispiel zeigt meine Umstellung.
// Lazy public function index() { return new PostCollection(Post::all()); } // Eager public function index() { return new PostCollection(Post::with(['author','comments', 'comments.author'])->get()); }
Gerade der Request auf alle Posts war davon sehr stark betroffen. Da die Laravel Resources für jeden Post den Author und die Kommentare einzeln nachgeladen haben hatte zeitweise 450 Abfragen beim Abfragen der Post-Liste (je nachdem wieviele Kommentare ich an die Posts angehängt hatte).
Kurz die Erklärung der Abfragen für 10 Posts mit je 5 Kommentaren:
Gesamt | 71 | Gesamt | 4 | |
Lazy Loading | Eager Loading | |||
Abfrage | Anzahl Abfragen | Abfrage | Anzahl Abfragen | |
Hole alle Posts | 1 | Hole alle Posts | 1 | |
Hole für jeden Post den Author | 10 | Hole alle Authoren zu allen Posts | 1 | |
Hole für jeden Post die Kommentare | 10 | Hole alle Kommentare zu allen Posts | 1 | |
Hole für jeden Kommentar den Author (5 Kommentare mal 10 Posts) | 50 | Hole alle Authoren zu allen Kommentaren | 1 |
Heißt das, dass wir lieber kein Lazy-Loading nutzen sollten? Nein!
In diesem Fall hat mir die Umstellung auf Eager-Loading zu einen Performance-Schub verholfen und mir damit vermutlich auch einige fiese Kommentare meiner lieben Kollegen erspart. 🙂 Insgesamt sollten wir aber von Fall zu Fall entscheiden, welches Vorgehen für uns das richtige ist.
Zusammenfassung
Ich hatte eingangs ein paar Fragen gestellt, die ich mir mit diesem Projekt beantworten wollte.
- Wie schnell kann ich das Projekt umsetzen?
- Der HeadlessBlog war nach gut 4h einsatzbereit, hat aber performancetechnisch noch Luft nach oben gehabt.
- Komme ich ohne Schwierigkeiten durch?
- Da hier keine größeren Besonderheiten auftraten, kam ich auch ganz ordentlich durch. Einzig die Performance hat mich ein wenig ins Schwimmen gebracht.
- Wie schneidet Laravel gegenüber den anderen Projekten ab?
- Nach meinen Fixes, konnte ich mit allen „Konkurrenz-Projekten“ mithalten.
- Ist der HeadlessBlog innerhalb eines Tages umsetzbar?
- Der Blog ist innerhalb eines Tages umsetzbar, wenn man die Sprache und das Framework kennt. Wenn da noch eine Einarbeitung erforderlich ist, dann wird die Zeit eher knapp.
- Genügt die Definition der API, um den Blog umsetzen zu können?
- Es gab noch ein paar unklare Punkte in der Definition die wir während des Projektes gemeinsam gefixed haben.
- Sind die API-Test ausreichend gut definiert, um sicherstellen zu können, dass die Projekte vergleichbar sind?
- Die API-Tests waren letztlich unser ausschlaggebendes Kriterium, ob der HeadlessBlog „korrekt“ umgesetzt wurde. Auch hier haben wir im Laufe des Tages ein paar Anpassungen vorgenommen.
Die Umsetzung diesen kleinen Projektes hat mir und uns als Team gezeigt, dass wir mit Laravel schnell und ordentlich ans Ziel kommen können. Damit kann ich sagen, dass Laravel beim WTL zurecht zu den Basic-Tools in der Webentwicklung gehört.