# Laravel im Einsatz [TOC] ## Wichtige Punkte * Versionen beachten: Wie auch bei PHP selbst kann sich das Verhalten von Laravel über verschiedene Versionen entscheidend ändern. Im weiteren wird die derzeit aktuelle Version 8 vorausgesetzt, aber viele Dinge gelten genau so in anderen Versionen. Es sollte aber bei Verweisen auf die [Laravel Docs](https://laravel.com/docs) sichergestellt werden, dass die gewünschte Funktionalität auch in der tatsächlichen Version vorhanden ist. * Auf der Kommandozeilen-Ebene stellt Laravel [Artisan](https://laravel.com/docs/8.x/artisan) bereit, mit dem sich viele Aspekte der gesamten Anwendung steuern lassen. In Windows werden die Artisan-Befehle mit Hilfe von `php artisan <Kommando>` gegeben, in Linux oder einer Bash-Umgebung (z.B. von git) kann das Artisan-Skript ausführbar gemacht werden, dann lautet der Aufruf `./artisan <Kommando>`. Das funktioniert jeweils im Root-Verzeichnis der Anwendung. * PHP bringt seit Version 5.4 einen [einfachen Webserver](http://php.net/manual/en/features.commandline.webserver.php) mit, dieser kann mit dem Befehl `php artisan serve` gestartet werden – dies ist aber nicht für den Produktiveinsatz gedacht. Serverfehler werden hierbei im Unterverzeichnis `/storage/logs` des Projekts gespeichert. * In aktuellen Versionen bringt Laravel das Package [Laravel Sail](https://laravel.com/docs/8.x/sail) mit, dass als Schnittstelle verwendet werden kann, um eine Entwicklung in Docker-Containern zu ermöglichen. Falls mit Laravel Sail gearbeitet wird, erfolgt der Zugriff auf Artisan und NPM mit Hilfe von `sail arisan ...` bzw. `sail npm ...` (wenn man ein [Bash Alias](https://laravel.com/docs/8.x/sail#configuring-a-bash-alias) eingerichtet hat). Trotzdem werden die PHP-Pakete und Comoser selbst auch lokal gebraucht, und die Docker-Volumes werden so eingebunden, dass die Dateien trotzdem lokal gespeichert werden (nicht allerdings die Datenbank). Standardmäßig hat man neben Laravel noch einen MySQL-Container, Mailhog (ein System, dass zu versendende Mails statt dessen abfängt und erlaubt, diese unter einem eigenen Port des Servers einzusehen), und einige weitere Dienste. Die Datenbank kann einfacher verwaltet werden, wenn auch ein [Phpmyadmin Docker Image](https://hub.docker.com/_/phpmyadmin) installiert wird. * Als Paketverwaltung unter PHP ist [Composer](https://getcomposer.org/) bei Laravel unentbehrlich. Das [Paketverzeichnis von Composer](https://packagist.org/) beinhaltet auch [Laravel selbst](https://packagist.org/packages/laravel/framework). Daneben gibt es auch ein [Paketverzeichnis speziell für Laravel](https://packalyst.com/). Auch z.B. [Laravel Collective HTML](https://laravelcollective.com/docs/6.x/html), eine sehr mächtige Ergänzung für HTML und Forms, kann über Composer installiert werden. * Eine [feste Verzeichnisstruktur](https://laravel.com/docs/8.x/structure) macht es einfacher, sich in bisher unbekannten Laravel-Projekten zurechtzufinden * Wenn innerhalb der Anwendung auf gespeicherte Daten zugegriffen werden soll, sollte der lokale Speicherbereich vom `public`-Verzeichnis aus [zugreifbar sein](https://laravel.com/docs/8.x/filesystem#the-public-disk) ## Node.js Node Packages werden auch gerne mit Laravel zusammen verwendet, also sollte auch [NPM](https://www.npmjs.com/) vorhanden sein. Vor allem, wenn [Laravel Mix](https://laravel-mix.com/) verwendet wird, ist das nötig. Ein Use Case hierfür ist, dass das `public`-Verzeichnis selbst über `.gitignore` von der Versionierung ausgeschlossen ist, aber dort benötigte Dateien an anderer Stelle im Projekt liegen und von Laravel Mix an die benötigte Stelle kopiert werden. Ebenso können CSS und JavaScript-Dateien von Laravel Mix minimiert oder gebündelt werden. ## Eloquent Laravel verwendet als [ORM](https://de.wikipedia.org/wiki/Objektrelationale_Abbildung) das Modul [Eloquent](https://laravel.com/docs/8.x/eloquent). Bei der Datenbank-Anbindung müssen zwei Stufen unterschieden werden, die beide benötigt werden: Das Erstellen oder Ändern eines Schemas wird über Migrationen erledigt, das kann als eine Versionierung des Datenbankschemas verstanden werden. Jede Migrationsdatei besteht aus einer `up`- und einer `down`-Methode, die `up`-Methode führt die Migration durch und die `down`-Methode nimmt sie zurück. Das bestehende Datenbankschema wird über [Model-Klassen](https://laravel.com/docs/8.x/eloquent#generating-model-classes) verwendet, wobei ein Model dem Schema einer Tabelle entspricht. Wird in den Models nichts anderes angegeben, werden außerdem [zwei Zeitstempel-Attribute](https://laravel.com/docs/8.x/eloquent#timestamps) den Schemata hinzugegeben, der Zeitpunkt, an dem ein Eintrag erstellt wurde (`created_at`), und der Zeitpunkt, an dem der Eintrag zuletzt geändert wurde (`updated_at`). In den Migrations allerdings müssen diese Spalten [explizit hinzugefügt](https://laravel.com/docs/8.x/migrations#column-method-timestamps) werden, dieser unterschiedliche Default sollte auf jeden Fall berücksichtigt werden. Wenn während der Entwicklung Spalten geändert werden können sollen, sollte [auf jeden Fall](https://laravel.com/docs/8.x/migrations#prerequisites) das Package `doctrine/dbal` dem Projekt hinzugefügt werden. Außerdem bietet das ORM besondere Funktionen an, um [Schemata miteinander zu verknüpfen](https://laravel.com/docs/8.x/eloquent-relationships). Aufrufe, die mehrere Ergebnisse zurückliefern können, werden als [Collections](https://laravel.com/docs/8.x/collections) zurückgegeben, wobei es sich um Objekte handelt, die ein zugrunde liegendes Array mit zusätzlichen Methoden "anreichern". Wird das Array selbst gewünscht, kann es auch [ausgegeben werden](https://laravel.com/docs/8.x/collections#method-all). Umgekehrt können auch Arrays in Collections [umgewandelt werden](https://laravel.com/docs/8.x/helpers#method-collect). ### Besondere Migrations Bestimmte Migrations sind mit besonderem Aufwand verbunden. Vor allem eine Umstellung von `nullable` auf `not null` kann nur durchgeführt werden, wenn kein Datensatz für diesen Wert `null` ist. Auch die umgekehrte Umstellung hat im Prinzip dieses Problem, dann aber für die `down`-Funktion. Für einfache Variablentypen wie `integer` oder `varchar`, kann man einfach einen Default festlegen und vor der Umstellung ein ```php= DB::statement('UPDATE <Table> SET <Field> = <Default> WHERE <Field> IS NULL'); ``` durchführen. Schwieriger ist es bei einem Fremdschlüssel, wo die zugehörige ID für einen gewählten Default nicht unbedingt bekannt ist, wenn der gleiche Eintrag überhaupt auf allen System existiert, die mit dem Programm arbeiten. Grundsätzlich ist es für diesen Fall sehr empfehlenswert, während der gesamten Migration [die Foreign Key Constraints abzuschalten](https://laravel.com/docs/8.x/migrations#toggling-foreign-key-constraints) (sonst kommt es hier sehr schnell zu Fehlern), und wenn man ein sinnvolles Default festlegen kann, lautet des SQL-Statement in etwa ```sql= UPDATE <Table> SET <Field> = (SELECT id FROM <ForeignTable> WHERE <ForeignTable>.<OtherField> = <Default> LIMIT 1) WHERE <Field> IS NULL; ``` Ist ein gemeinsamer Default nicht vorhanden, sucht man einen vorhandenen Fremdschlüssel mit ```sql= SELECT id FROM <ForeignTable> LIMIT 1; ``` was allerdings die konkrete Belegung zufällig macht. ### Datumsfelder Bei Datumsfeldern gibt es einige Besonderheiten zu beachten. Vor allem auf zwei Probleme kann man immer wieder stoßen: * Datumswerte werden standardmäßig als JAHR-MONAT-TAG formatiert (internationaler Standard), womit allerdings viele Leute hierzulande nicht gut umgehen können. * Als Zeitzone verwendet PHP UTC, und es ist ein sehr mühsames Unterfangen, dies PHP komplett abzugewöhnen. Normalerweise wird dies nicht empfohlen, aber es gibt einen Bereich, wo dieser Standard einem viel Ärger bereiten kann: Wenn man nur an Datumswerten interessiert ist. Ohne Zeitangabe wird Mitternacht angenommen, dann wird der Wert (für die interne Speicherung) auf UTC übertragen, und schon hat man den Vortag gespeichert. Für das erste Problem gibt es eine Reihe von JS-Bibliotheken, die einen besseren Umgang mit Datumswerten versprechen. An dieser Stelle sollte man auf jeden Fall wissen, ob man den [Datatables Editor](#Datatables-Editor) einsetzen will, denn dann sollte die gewählte Bibliothek auch diesen unterstützen. Der Datatables Editor selbst [empfiehlt](https://editor.datatables.net/reference/field/datetime#Description) die Bibliothek [Moment.js](http://momentjs.com/), die von deren Entwicklern allerdings [als veraltet](https://momentjs.com/docs/#/-project-status/) angesehen wird (vor allem, weil die Objekte mutable sind). Für viele Fälle reicht sie allerdings aus (man sollte für einen nicht-englischen Einsatz allerdings die Locals mitinstallieren), dann reicht die Zeile ```javascript= $.fn.dataTable.moment('DD.MM.YYYY'); ``` zu Beginn aus, und die Datatables können Datumswerte korrekt sortieren. Datumswerte kann man innerhalb von Javascript auf das deutsche Format konvertieren mittels ```javascript= new Date(date).toLocaleDateString( 'de-DE', { month: '2-digit', day: '2-digit', year: 'numeric' } ); ``` und wenn man bei einer Uhrzeit die Sekunden abschneiden will, geht das mit ```javascript= new moment(time, 'HH:mm:ss').format('HH:mm'); ``` unter Verwendung der Moment.js-Bibliothek. Was verschiedene Zeitzonen angeht, so ist das eigentliche Problem, dass die Standards an verschiedenen Stellen verteilt sind. Laravel kann einen Standard setzen, ebenso PHP, und Server wie Apache haben auch noch eigene Einstellmöglichkeiten. In der Regel stehen alle diese Werte per Standard auf UTC, und wenn man nicht alle diese Einstellungen ändert, bekommt man zum Teil merkwürdige Fehler. Der von Laravel verwendete Standard-Datetime-Helper Carbon [empfiehlt ebenfalls](https://carbon.nesbot.com/docs/#api-instantiation), mit UTC zu arbeiten. Leider hat PHP keinen reinen Date-Typen (im Gegensatz zu z.B. MySQL), so dass man bei reinen Datumswerten schnell auf das oben beschriebene Problem kommt. Was man machen kann, ist tatsächlich (auch für Datumswerte) mit UTC zu arbeiten, und bei der Übertragung an den Client verwendet man dann ```php= $date->setTimezone('Europe/Berlin')->format('Y-m-d'); ``` und innerhalb des Clients wird der Wert dann auf die oben beschriebene Weise in das deutsche Format übertragen. Das Datum zuerst auf diese Weise an den Client zu übertragen, ist z.B. dann wichtig, wenn es sich um einen Standardwert für ein `<input type="date">`-Element handelt, denn dieses erwartet das `value`-Attribut [in diesem Format](https://stackoverflow.com/a/14212715). ## Routing Ein wichtiger Einstiegspunkt, um ein bestehendes Projekt zu verstehen, ist das [Routing](https://laravel.com/docs/8.x/routing). Hier wird (bei Laravel in der Regel [innerhalb einer Datei](https://laravel.com/docs/8.x/routing#the-default-route-files)) für jede Route ggf. mit Platzhaltern und der gewünschten HTTP-Methode festgelegt, wie das Programm auf die Route reagiert. In der Regel wird dabei die Methode eines Controllers angegeben. Laravel arbeitet nach dem [MVC-Prinzip](https://de.wikipedia.org/wiki/Model_View_Controller). Die Modell-Klassen stellen über ORM die Datenbank bereit, die Views bauen auf der Blade Template Engine auf, und die Controller steuern die Anwendung, wenn sie über das Routing aufgerufen werden. Ein für *GET* aufgerufener Controller stellt in der Regel einige Parameter bereit, die ggf. noch berechnet werden, und ruft dann eine View auf. Controller für *POST*, *PUT*, *PATCH* oder *DELETE* hingegen führen einen damit verbundenen Auftrag aus und geben danach über [JSON](https://laravel.com/docs/8.x/responses#json-responses) das Resultat zurück oder führen ein [Redirect](https://laravel.com/docs/8.x/responses#redirects) aus. Ein [typischer Satz von Controllern für ein Schema](https://laravel.com/docs/8.x/controllers#actions-handled-by-resource-controller) kann mit dem Befehl `Route::resource(<Name>, <Controller-Klasse>)` erstellt werden. Eine genauere Beschreibung der verschiedenen HTTP-Methoden findet sich hier: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods - hiervon werden vor allem *GET*, *POST*, *PUT*, *PATCH* und *DELETE* benötigt. Vergleiche dazu auch den „Satz von Controllern“ (siehe oben). Kurz gesagt: *GET* sind reine Leseaufrufe, *DELETE* löscht eine Ressource, ein *POST* erfolgt an eine Kollektion von Daten, um ein neues Datum zu erstellen (ohne dass eine ID bekannt ist), *PUT* und *PATCH* ändern ein bestimmtes Datum. Der wesentliche Unterschied zwischen *PUT* und *PATCH* ist, dass [*PUT* idempotent sein soll](https://tools.ietf.org/html/rfc7231#section-4.2.2), das heißt, das Ergebnis ändert sich nicht, wenn ein Aufruf (ggf. mehrfach) wiederholt wird. Bei *PATCH* wird das hingegen [nicht garantiert](https://tools.ietf.org/html/rfc5789#section-2). Daher sind UPDATE-Aufrufe in der Regel *PUT*. Ein typisches Beispiel für *PATCH* wäre hingegen ein [Besucherzähler](https://de.wikipedia.org/wiki/Besucherz%C3%A4hler), der bei jedem neuen Aufruf den Zähler um 1 erhöht. ### Routenübergänge HTML-Links sind *GET*-Aufrufe. Die anderen Methoden können über eine FORM abgesetzt werden oder über JavaScript. Das letztere ist dabei flexibler, vor allem mit [jQuery](https://jquery.com/) lässt sich [sehr vielseitig ein solcher Funktionsaufruf erstellen](https://api.jquery.com/jquery.ajax/). In beiden Fällen ist zu beachten, dass diese Aufrufe vor [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) geschützt sind und daher ein [besonderes Sicherheitsattribut ](https://laravel.com/docs/8.x/csrf) mitbringen müssen. Im Falle von JavaScript bietet es sich an, den Auslöser als BUTTON zu schreiben und den Aufruf über eine onclick-Methode durchzuführen. Falls man einen *PUT*, *PATCH* oder *DELETE* Aufruf über eine FORM durchführt, muss ein [zusätzliches, verstecktes Feld](https://laravel.com/docs/8.x/routing#form-method-spoofing) der FORM hinzugefügt werden, weil HTML Forms diese Methoden sonst nicht unterstützen. Für die Verarbeitung einer Form empfiehlt sich zum Schluss ein Redirect. Falls man den [AJAX](https://de.wikipedia.org/wiki/Ajax_(Programmierung))-Aufruf von jQuery verwendet (bzw. [angepasste Fassungen mit vorbelegten Attributen](https://www.w3schools.com/jquery/jquery_ref_ajax.asp)), muss man allerdings bei der Datenrückgabe aufpassen, denn AJAX ist asynchron. Um die zurückerhaltenen Daten zu verarbeiten, kann man z.B. den Parameter `success` verwenden, der eine Funktion erwartet. Dieser Funktion können vor allem die zurückerhaltenen Daten im ersten Parameter mitgegeben werden. Auch die Weiterleitung an eine andere (*GET*-)Route ist dann möglich über ```javascript= document.location.href = <URL-String>; ``` Für eine Fehlerbehandlung steht der Parameter `error` zur Verfügung, der ebenso eine Funktion erwartet. Häufig ist es eine gute Idee, die Daten als JSON zu übertragen, da man so mehrere Parameter strukturiert an den Controller übergeben kann (sollen auch die zurückzugebenden Daten im JSON-Format sein, gibt man `dataType: "json"` an - auch das bietet sich in der Regel an). Außerdem kann es notwendig sein, das CSRF-Token auf diese Weise (als Attribut `_token`) zu übertragen, und dann ist die strukturierte Datenübergabe zwingend notwendig. Man kann allerdings auch einige Parameter als Teil der Route übertragen (wie es auch bei *GET*-Aufrufen üblich ist), diese sind dann allerdings als Teil der URL sichtbar und sollten darüber hinaus nicht zu lang sein. Werden ganze Objekte auf diese Weise übertragen, werden sie zunächst auf JavaScript-Seite mit ```javascript= json = JSON.stringify(<Objekt>); ``` in einen JSON-String umgewandelt, mit ```javascript= data: { attr: json } ``` übertragen und auf PHP-Seite mit ```php= $obj = json_decode($request->attr); ``` wieder in ein Objekt überführt (will man statt dessen ein assoziatives Array zurückgewinnen, [gibt man einen zweiten Parameter mit `true` an](https://www.php.net/manual/de/function.json-decode.php)). Hierbei wird `$request` als Request-Objekt im Funktions-Header aufgeführt und damit mittels Dependency Injection in den Controller überführt. Damit das funktioniert, muss die Klasse `Illuminate\Http\Request` im Kopf der Controller-Datei importiert werden: ```php= use Illuminate\Http\Request; ``` Hier werden die Ergebnisse zum Schluss als JSON zurückübertragen, und die aufrufende jQuery-Funktion muss jetzt entscheiden, ob der Aufruf erfolgreich war. Im Fall des Rückgabetypen JSON gilt: Das passiert *nicht* anhand verwendeter JSON-Schlüssel, sondern *jede* JSON-Antwort wird als Erfolg gesehen. Um also einen Fehler zurückzugeben, darf die Antwort gerade nicht im JSON-Format sein, sondern z.B. reiner Text. Ein Beispiel für eine Rückmeldung mit Erfolg wäre hier ```php= return response()->json(['keyA' => 'valueA', 'keyB' => 'valueB']); ``` und für einen Fehler ```php= return response('message', 400); ``` Der zweite Parameter bestimmt den HTTP-Statuscode, der [standardmäßig auf 200 gesetzt](https://github.com/illuminate/http/blob/v8.37.0/Response.php#L31) wird. [200 bedeutet OK](https://de.wikipedia.org/wiki/HTTP-Statuscode#2xx_%E2%80%93_Erfolgreiche_Operation), aber weil die Antwort nicht im JSON-Format ist, würde trotzdem die Antwort als Fehler ausgewertet. Das würde die Auswertung mit Hilfe von dev tools deutlich erschweren (häufig werden auf einer Seite eine ganze Reihe von HTTP Responses gebraucht, und alle erfolgreichen geben mit Sicherheit Status 200 zurück), deshalb ist es eine gute Idee, von Hand einen Fehler-Statuscode zu setzen. Im Allgemeinen sollte man auch nicht 500 nehmen, weil das der Statuscode ist, der in der Regel bei einem Bug automatisch zurückgeliefert wird. Statt dessen sollte man sich genau überlegen, [welcher Statuscode](https://de.wikipedia.org/wiki/HTTP-Statuscode#Liste_der_HTTP-Statuscodes) die Fehler-Situation am genauesten beschreibt. In der Regel sind derartig zu verarbeitende Situationen [Client-Fehler](https://de.wikipedia.org/wiki/HTTP-Statuscode#4xx_%E2%80%93_Client-Fehler), während [Server-Fehler](https://de.wikipedia.org/wiki/HTTP-Statuscode#5xx_%E2%80%93_Server-Fehler) eher Bugs sind, die man beheben sollte. Die Funktion, die im `success` bzw. `error` Element benannt wird, bekommt in der Regel einen Parameter mit. Für `success` ist das `data`, was im Fall einer JSON-Response dieses JSON als Objekt beinhaltet. Mit den Schlüsselwerten als Attribute kann auf die Information zurückgegriffen werden. Bei `error` handelt es sich um ein `jqXHR`-Objekt, und ein zurückgegebener Text kann [mit dem Attribute `responseText`](https://api.jquery.com/jQuery.ajax/#jqXHR) ausgelesen werden. Ein zusätzlicher Gesichtspunkt, der gegen die Verwendung einer Form sprechen kann, ist folgender: Jede Form hat genau eine feste Form-Action, und jedes Input-Feld ist (höchstens) einer Form zugeordnet. Wenn man also zwei verschiedene Aktionen hat, die den gleichen Wert verarbeiten sollen, [ist das zwar möglich](https://stackoverflow.com/questions/1692564/two-forms-share-same-input/39023485), aber ziemlich umständlich. ## Validierung Wenn die Daten z.B. eines Formulares serverseitig validiert werden sollen, sollte statt eines allgemeinen Requests (s.o.) ein angepasstes Request-Objekt übergeben werden, das zusätzlich Validierung beinhaltet. Dieses wird von der Klasse `Illuminate\Foundation\Http\FormRequest` abgeleitet und beinhaltet in der Regel die folgenden Funktionen: * authorize(): Eine bool-wertige Funktion, die bestimmt, ob der Nutzer diesen Aufruf durchführen darf * rules(): Diese Funktion gibt ein assoziatives Array zurück mit den Parameter-Namen als Schlüsseln. Hier wird (ggf. abhängig von der HTTP-Methode) bestimmt, welche [Einschränkungen für die einzelnen Parameter](https://laravel.com/docs/8.x/validation#available-validation-rules) gelten * messages(): Hier können besondere Fehlermeldungen definiert werden, die abhängig vom Fehlertyp sind. Der Rückgabewert ist ein assoziatives Array mit den Fehlertypen als Schlüssel. ## Helper-Funktionen Wenn man bestimmte Funktionen unabhängig von Model-Klassen in der ganzen Anwendung braucht, bietet es sich an, [eine eigene Helper-Klasse](https://laravel-news.com/creating-helpers) zu schreiben, in der diese Funktionen untergebracht werden können. Auf diese Weise lassen sich diese Funktionen auch ohne eine Klassenreferenz direkt aufrufen. Diese Verfahrensweise bietet sich auch an, um globale Konstanten in der Anwendung haben zu können. ## Tabellen Ein wichtiger Bestandteil von Applikationen mit [CRUD](https://de.wikipedia.org/wiki/CRUD)-Funktionalität sind Index-Seiten, wo der vorhandene Bestand an Objekten einer Klasse aufgelistet wird. Dies geschieht üblicherweise mit Hilfe einer Tabelle. Falls im Projekt bereits jQuery verwendet wird (z.B. für AJAX-Aufrufe, s.o.), bietet es sich an, für die Ausgabe von Tabellen auf das [DataTables Plugin](https://datatables.net/) von jQuery zurückzugreifen. Mit Hilfe von jQuery erfolgt von der View aus der [AJAX-Abruf](https://datatables.net/manual/ajax), und im Controller werden über das [zugehörige Serverpaket](https://datatables.yajrabox.com/) die Daten aufbereitet und an die View geschickt (die entsprechende Controller-Methode muss eine eigene *GET*-Route zugewiesen bekommen, die dem AJAX-Aufruf mitgegeben wird). Das Serverpaket selbst wird [mit Hilfe von Composer installiert](https://yajrabox.com/docs/laravel-datatables/master/installation). Auch HTML-Elemente können in eine solche Tabelle eingebunden werden, allerdings muss dann das HTML-Escaping [ausgeschaltet](https://yajrabox.com/docs/laravel-datatables/master/raw-columns) werden. Die meisten HTML-Elemente lassen sich auf diese Weise einfach einbinden, einschließlich HTML-Links. Falls die Elemente allerdings an Ereignisse gebunden werden sollen (z.B. Buttons), sollte man bei der Event-Bindung [Delegated Events](https://learn.jquery.com/events/event-delegation/) verwenden, denn sonst werden nur die Elemente gebunden, die zu dem Zeitpunkt des Aufrufs vorhanden sind, was z.B. Paginierung zu einem Problem macht. Für das Datatables-Package gibt es noch [verschiedene Erweiterungen](https://datatables.net/extensions/index). Die meisten davon sind problemlos zu verwenden, allerdings ist [eine, die das direkte Editieren von Zellen zulässt](https://editor.datatables.net/), kostenpflichtig. ### Datatables Editor Falls man Zugriff auf diese Erweiterung hat, werden allerdings viele CRUD-Zugriffe deutlich einfacher. Voraussetzung dafür ist, dass das Editor-Package richtig installiert ist. Während dies beim client-seitigen Teil kein Problem ist, gibt es für den server-seitigen Teil eine Entscheidung zu treffen. * Datatables Editor bringt selbst ein PHP Package mit, dessen [Installation](https://editor.datatables.net/manual/php/installing) allerdings nicht an Laravel angepasst ist. Man kann dieses Package verwenden, bei der Installation muss man allerdings dafür sorgen, dass die Package-Dateien auch geladen werden. Hierzu bietet es sich an, die Package-Dateien analog zu der [eigenen Helper-Klasse](#Helper-Funktionen) einzurichten. * Daneben gibt es auch ein [externes Server-Package](https://yajrabox.com/docs/laravel-datatables/master/editor-installation), das mit Laravel direkt verwendet werden kann, allerdings nicht von dem Editor-Herausgeber ist und deshalb manchmal (noch) nicht alle Editor-Funktionen unterstützt. Die Funktionalität ist allerdings schon weit fortgeschritten, und wenn dieses Package die benötigte Funktionalität bereitstellt, spricht einiges dafür, es zu verwenden. Im weiteren wird das externe Server-Package vorausgesetzt. Zunächst allerdings die Einbindung der Client-Seite: #### Client Auf der Seite des Datatables Editor gibt es [viele Beispiele](https://editor.datatables.net/examples/index), die einem bei der Einrichtung helfen können. Der Editor setzt in aller Regel eine Datatable voraus. Man kann zwar den Editor auch außerhalb einer Datatable verwenden ("Standalone"), aber dann wird es schwieriger, Daten für die entsprechenden Felder einzulesen, während die rein lesende Seite gut von einer Datatable verwaltet werden kann. Zunächst einmal hat man innerhalb des HTML etwas wie ```html= <table class="table" id="theTable"> ``` stehen, womit man jetzt `#theTable` als Referenz verwenden kann. Datatable und Editor können dann im Javascript-Bereich erzeugt werden, die Datatable mittels ```javascript= $('#theTable').DataTable({ ajax: '<Datatable AJAX URL>', columns: [ ... ] ... }); ``` der Editor mittels ```javascript= let editor = new $.fn.dataTable.Editor({ ajax: '<Editor URL>', table: '#theTable', fields: [ ... ] }); ``` In beiden Fällen kann das AJAX-Attribut auch ein Objekt sein, was sich vor allem dann anbietet, wenn man keinen *GET*-Aufruf macht. In diesem Fall steht jeweils die eigentliche URL im Unter-Attribut `url`, in `type` steht die zu verwendene HTTP-Methode. Wird die Methode nicht angegeben, so ist sie standardmäßig für die Datatable *GET* und für den Editor *POST*. Komplexere Anfragen - auch an eine Datatable - sollten allerdings als *POST* abgesetzt werden, weil *GET*-Aufrufe [Probleme mit zu langen URIs](https://stackoverflow.com/a/2659995) haben können. Das Problem kann oft auftreten, wenn mit [ServerSide Rendering](https://datatables.net/reference/option/serverSide) gearbeitet wird, weshalb die Entwickler des Editors hierfür auch die Umstellung auf *POST* [empfehlen](https://editor.datatables.net/examples/simple/server-side-processing.html). Weitere Parameter (z.B. das CSRF-Token) können mit Hilfe des Attributs `data` eingebunden werden: ```javascript= data: { '_token': '{{ csrf_token() }}' }, ``` Das Attribut `data` kann auch eine einparametrige Funktion festsetzen, dieser eine Parameter (als Objekt) erlaubt dann, auch komplexere Festlegungen zu machen. Das `columns`-Attribut der Datatable und das `fields`-Attribut des Editors legen die einzelnen Spaltenwerte fest. Hierbei ist es auch möglich (z.B. für `enum`-artige Werte, die mit einem `<select>` geändert werden sollen), unterschiedliche Attribute für die Datatable und für den Editor zu verwenden (den Text für die Datatable, die ID für den Editor) mittels der [Option `editField`](https://datatables.net/reference/option/columns.editField) innerhalb von `columns`. Wenn der Editor vor der Datatable in Javascript definiert wird, kann auf ihn zurückgegriffen werden. Der Editor definiert z.B. eine Reihe von Knöpfen, die in die Datatable eingebunden werden können (vor allem zum Erstellen, Bearbeiten und Löschen), und wenn der Editor vorher definiert und in einer Variable gespeichert wurde, lässt sich ein solcher Schalter z.B. mittels ```javascript= buttons: { buttons: [ extend: 'create', editor: editor ] } ``` definieren (der Buttons-Bereich muss dann noch mit Hilfe der [`dom`-Option](https://datatables.net/reference/option/dom) aktiviert werden, dieser Bereich wird hier mit einem `B` dargestellt, z.B. `dom: 'Bflrtip'`). Die Editor-Funktionen können auch an selbstgebaute Buttons angebunden werden, hierfür können innerhalb eines Click-Handlers die [API-Funktionen des Editors](https://editor.datatables.net/manual/api#Editing) aufgerufen werden. Eine häufige Fehlerquelle ist, dass für neuerstellte Einträge der *Datatable* die entsprechenden Daten nicht vorliegen, bevor ein AJAX-Abruf durchgeführt wurde. Um das Problem zu lösen, muss man zum einen den einzelnen Spalten der Tabelle einen [default-Wert](https://datatables.net/reference/option/columns.defaultContent) geben (am besten einfach `''`), und dann sorgt man dafür, dass nach jedem `create` und jedem `edit` die Tabellendaten neu geladen werden (auch die Table sollte dafür in einer Variable gespeichert sein, hier `table`): ```javascript= editor.on('create', function(e, json, data) { table.ajax.reload(); }); editor.on('edit', function (e, json, data, id) { table.ajax.reload(); }); ``` #### Server Für die Bereitstellung des Servers wird hier das [externe Package](https://yajrabox.com/docs/laravel-datatables/master/editor-installation) verwendet, dass eine nahtlose Verwendung in Laravel erlaubt. Ist dieses Package erst einmal installiert, funktioniert die Verwendung folgendermaßen: Kernstück des Editor-Servers ist eine Editor-Klasse, die mit [einem Artisan-Kommando](https://yajrabox.com/docs/laravel-datatables/master/editor-command) direkt angelegt werden kann. Diese Klasse wird in `app/DataTables` angelegt. Zunächst passt man das `$model`-Attribut an das [Model an, dass dem Editor zugrunde liegt](https://yajrabox.com/docs/laravel-datatables/master/editor-model). Dann legt man für die 4 möglichen Fälle `create`, `edit`, `remove` und `upload` [Validierungsregeln fest](https://yajrabox.com/docs/laravel-datatables/master/editor-rules) (ähnlich denen [von Laravel selbst](#Validierung)), und schließlich kann man noch [eine Reihe von Hooks verwenden](https://yajrabox.com/docs/laravel-datatables/master/editor-events), durch die die einzelnen Werte noch weiter verarbeitet werden können oder die Werte anderer Modelle an etwaige Änderungen angepasst werden können. Zuletzt muss dann noch der Zugriff auf diese Editor-Klasse [im Routing oder in einer Controller-Funktion](https://yajrabox.com/docs/laravel-datatables/master/editor-usage#register-route) festgelegt werden. Die URL-Route dieser Controller-Funktion ist dann genau die, die auf Client-Seite dem Editor-Objekt übergeben wird. ## Flash-Nachrichten Eine gute Möglichkeit, dem Anwender mitzuteilen, ob eine Schreibaktion erfolgreich war, sind Flash-Nachrichten. Hierfür gibt es einige gute Composer-Pakete, z.B. [laracasts/flash](https://packagist.org/packages/laracasts/flash). Falls allerdings [Bootstrap](https://getbootstrap.com/) Bestandteil des Projekts ist, lässt sich eine solche Flash-Nachricht auch [selbst bauen](https://getbootstrap.com/docs/4.0/components/alerts/#dismissing), die Daten hierfür kann der Controller der View mittels der [Session-Daten](https://laravel.com/docs/8.x/responses#redirecting-with-flashed-session-data) mitteilen. ## Administrative Funktionen Wenn eine Web-Anwendung ein Nutzer-System hat, dann hat sie auch Nutzer mit besonderen Funktionen, insbesondere Administratoren. Diesen stehen einige Funktionalitäten zur Verfügung, die für normale Nutzer gesperrt sein sollen. Laravel bringt ein [Scaffolding-System](https://laravel.com/docs/8.x/installation#the-laravel-installer) mit, dass wichtige Grundfunktionen direkt erzeugt. Dazu gehört auch ein grundlegendes User-System, für das auch bereits erste Datenbank-Tabellen mit den dazugehörigen Model-Typen erzeugt werden. Dies reicht bereits aus, um bestimmten Usern Administratoren-Status zu geben. Diese erhalten also innerhalb der Anwendung einige besondere Funktionen, um z.B. neue User anlegen zu können. Damit "normale" User diese Funktion nicht haben, ist zweierlei nötig: * Die entsprechenden Links / Buttons dürfen diesen Usern nicht angezeigt werden * Ein direkter Aufruf dieser Funktionen durch diese User über URL muss ebenfalls verhindert werden Führt man nur die erste Maßnahme durch, ist das System nicht sicher. Die meisten dieser Routen-Bezeichnungen sind einfach zu erraten, und man möchte sich den Umgang mit der Anwendung nicht unnötig verkomplizieren. Wenn man hingegen nur die zweite Maßnahme durchführt, erhalten die "normalen" User tote Links, was zu einer schlechten Nutzererfahrung führt. Um die Links / Buttons ausblenden zu können, wenn kein berechtigter User eingeloggt ist, muss zuerst bestimmt werden, welcher User eingeloggt ist. Dafür stellt Laravel die Hilfsklasse `Auth` zur Verfügung, die mit `use Auth;` eingebunden werden kann. Danach erhält man den eingeloggten User (als Objekt) mit `Auth::user()`, kann also Tests wie `if (Auth::user()->hasRole('admin'))` durchführen, so dass entsprechende Teile der Anwendung ausgeblendet werden können. Auf die gleiche Weise kann man den direkten Aufruf unterbinden, wenn man einen solchen Test zu Beginn der Controller-Funktion durchführt. Alternativ kann man auch in der Routing-Datei die entsprechenden Routen unter Bedingungen stellen. Dazu richtet man [Middleware](https://de.wikipedia.org/wiki/Middleware)-Klassen unter `Http\Middleware` in [`App`](https://laravel.com/docs/8.x/structure#the-app-directory) ein. Diese beinhalten eine Funktion ```php= public function handle($request, Closure $next) { ... } ``` in der entschieden wird, ob ein entsprechender Aufruf zulässig ist (in dem Fall wird der Aufruf an `$next` weitergereicht) oder abzubrechen ist (durch einen Redirect). Jetzt kann man innerhalb der Routing-Datei entweder an die einzelne Routenanweisung z.B. `->middleware('admin')` anhängen, oder eine ganze Gruppe von Routen umgeben mit z.B. ```php= Route::group(['middleware' => ['admin']], function () { ... }); ``` Für einzelne Routen mag der zusätzliche Aufwand mit einer Middleware unnötig erscheinen, aber dies ist eine bessere Trennung der Zuständigkeiten und die einzelne Middleware lässt sich auch wiederverwenden. Hier kann man auch komplexere Anforderungen stellen, z.B. Rolle A *oder* Rolle B. ## Dateien hochladen Um aus der Anwendung Dateien hochladen zu können, empfiehlt sich der Einsatz einer Form, wobei allerdings einige Besonderheiten zu beachten sind. Wenn man Laravel Collective HTML ([siehe oben](#Wichtige-Punkte)) einsetzt, kann man ein [Formular für den Datei-Upload](https://www.tutorialspoint.com/laravel/laravel_file_uploading.htm) schnell aufsetzen. Aber auch mit "einfachem" PHP lässt sich ein [solches Formular vorbereiten](https://www.w3schools.com/php/php_file_upload.asp). Allerdings sollten hier noch einige Konfigurationen vorgenommen werden. Zum einen sollte der Dateityp festgelegt werden (um dem User die Dateiauswahl zu erleichtern und [aus Sicherheitsgründen](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html)). Dafür gibt man im `accept`-Attribut des `input file`-Elements die zu akzeptierenden [MIME-Types](https://wiki.selfhtml.org/wiki/MIME-Type/%C3%9Cbersicht) an als String, wobei die einzelnen Einträge durch Kommata getrennt werden, z.B. für den Upload von verschiedenen Bild-Formaten: ```htmlembedded= <input type="file" name="datei" accept="image/gif,image/jpeg,image/png"> ``` Alternativ können die zulässigen Endungen mit führendem `.` angegeben werden. Um den Server nicht zu überlasten, sollte man außerdem eine maximale Größe für die Dateien festlegen, die hochgeladen werden dürfen. Dazu benutzt man die serverseitige Validierung ([siehe oben](#Validierung)) und setzt in der `rules()`-Funktion für das Bild eine Bedingung `max:<Größe in KB>` fest. Die Datei erreicht den Server als Objekt vom Typ [UploadedFile](https://laravel.com/api/8.x/Illuminate/Http/UploadedFile.html), das mit den [Laravel Filesystem](https://laravel.com/docs/8.x/filesystem#file-uploads)-Methoden auf dem Server abgespeichert werden kann. Das `input file`-Element selbst verfügt zwar über die volle Funktionalität auf UI-Seite, die gebraucht wird, hat aber selbst noch einige Einschränkungen, die man kennen sollte. Zunächst einmal ist es [aus Sicherheitsgründen](https://forum.selfhtml.org/self/2005/feb/22/formulare-upload/768908#m768908) nicht möglich, den Wert des input-Elements vorzubelegen (weshalb hier auch kein [Affenformular](https://wiki.selfhtml.org/wiki/Affenformular) möglich ist). Darüber hinaus ist es nicht vorgesehen, das Styling dieses Elements in irgendeiner Weise zu ändern, und das vorbelegte Styling sorgt für ein relativ großes Element, das sich nicht wirklich in ein vorgegebenes Seitendesign einfügt, deshalb muss man hier zu einigen Tricks greifen. Eine Möglichkeit besteht darin, dass `input file`-Element zu verstecken, ein anderes clickbares Element anzuzeigen, und den Click per JavaScript weiterzureichen mittels einer Funktion wie ```javascript= function clickFileButton() { $('#uploadBtn').click(); } ``` Um das `input file`-Element zu verstecken, gibt es im Prinzip [3 Möglichkeiten](https://stackoverflow.com/a/1511884). Allerdings gibt es bei der zweiten und dritten den Nachteil, dass der Platz für das Element reserviert bleibt und damit eine Lücke in der Anzeige entsteht. Bei der dritten Möglichkeit gibt es den zusätzlichen Nachteil, dass das Element selbst weiterhin geclickt werden kann, was in der Regel nicht gewünscht ist. Die erste Möglichkeit ```css= { display: none;} ``` funktioniert sehr gut, führt aber zu [Schwierigkeiten, wenn Screen Reader zum Einsatz kommen. Damit ergeben sich weitere Möglichkeiten, die umständlicher sind, aber die gewünschte Funktionalität mit Barrierefreiheit erreichen](https://css-tricks.com/places-its-tempting-to-use-display-none-but-dont/). ## Einstieg ### Routing Wie [weiter oben](#Routing) erläutert, ist das Routing normalerweise der Einstiegspunkt, um die Anwendung zu verstehen. Dies gilt unabhängig davon, ob man das Projekt von einem vorherigen Entwickler oder von dem Scaffolding-System übernimmt (bei einem neuen Projekt). Will man verstehen, wie die Anwendung auf einen Seitenaufruf reagiert, sieht man zuerst in der Datei `routes/web.php` nach, welche Reaktion auf diese Route (falls über URL eingegeben als *GET*-Aufruf) vorgesehen ist. Hier kann direkt eine Funktion vorgesehen sein, aber in der Regel steht hier eine Methode von einem Controller, häufig in der Syntax `'<Controller>@<Methode>'`. Diese Controller-Methoden findet man im `Controllers`-Verzeichnis unterhalb von [`app/Http`](https://laravel.com/docs/8.x/structure#the-http-directory), in einer gleichgenannten `public function`, wo auch die variablen Parameter der Route übergeben werden (sowie ggf. ein Request als Dependency Injection, s.o.). ### Controller Bei einer *GET*-Methode werden jetzt häufig einige Variablen belegt, und dann wird eine View aufgerufen, üblicherweise mit der Syntax ```php= return view('<View>', compact('<Parameter1>', '<Parameter2>', ...)) ``` Mit `<View>` wird die View übergeben, die verwendet werden soll. Hierbei handelt es sich um eine *Blade*-Datei, und das obligatorische Suffix `.blade.php` wird weggelassen. Die Datei selbst findet sich im `views`-Verzeichnis innerhalb des ['resources'](https://laravel.com/docs/8.x/structure#the-resources-directory)-Verzeichnisses. Eine zusätzliche Unterverzeichnisstruktur [wird *hier* mit Punkten](https://laravel.com/docs/8.x/blade#blade-directives) angegeben (weshalb man keine Punkte in den Verzeichnisnamen für die Views verwenden sollte). Die [compact](https://www.php.net/manual/de/function.compact.php)-Funktion bekommt eine variable Anzahl von Strings übergeben und erzeugt daraus ein assoziatives Array nach folgendem Muster: ```php= compact('varA', 'varB') = [ 'varA' => $varA, 'varB' => $varB ]; ``` Auf diese Weise werden Parameter an die View übertragen, allerdings wurde mit PHP 7.3 die `compact`-Funktion an einem entscheidenden Punkt verändert: Wird eine nicht belegte Variable übergeben, wurde vor PHP 7.3 diese Variable ignoriert, so dass der Wert in der View `undefined` war, genau wie im Controller. Seit 7.3 allerdings wird an der Stelle ein Fehler geworfen, wenn eine der Variablen `undefined` ist. Um im neueren PHP dieses Problem zu vermeiden, kann man die entsprechende Variable mit `null` vorbelegen, ansonsten muss man entweder mit Fallunterscheidungen arbeiten, was u.U. zu sehr vielen Einzelfällen führen kann, oder man verzichtet auf den Einsatz der `compact`-Funktion und übergibt statt dessen direkt das assoziative Array, das sonst von dieser Funktion gebildet würde. Handelt es sich nicht um einen *GET*-Aufruf, so gibt es am Ende der Verarbeitung drei Möglichkeiten: * es wird ein `back()` bzw. ein `redirect()->back()` zurückgegeben, womit die Kontrolle an die vorherige Route zurückgegeben wird und zwar [als *GET*](https://stackoverflow.com/a/45486946) * es wird ein `redirect` zu einer neuen Route gegeben, für welche man jetzt erneut das Routing untersucht und zwar [als *GET*](https://stackoverflow.com/a/45486946) * es wird eine `Response` (häufig eine JSON-Response) herausgegeben, in diesem Fall sollte die Seite über AJAX aufgerufen worden sein, dann geht man zu dieser AJAX-Methode zurück, wo die weitere Verarbeitung in der Funktion von dem `success` oder dem `error` Parameter angegeben wird ### View Die [View](https://laravel.com/docs/8.x/views) baut auf der [Blade Templating Engine](https://laravel.com/docs/8.x/blade) auf und ist grundsätzlich eine PHP-Datei mit einer Vielzahl zusätzlicher Kommandos. Die Template Engine generiert hieraus "richtige" PHP-Dateien. Zunächst einmal kann mit Hilfe von `@extends('<ParentView>')` ein Template vorgegeben werden, dass von dieser View befüllt wird (auch mehrstufig, also eine Hierarchie). In dem Template können Stellen vorgesehen sein mit `@yield('<Section>')`, die von der View gefüllt werden können. Dazu verwendet die View ```php= @section('<Section>') ... @endsection ``` in dem typischen Fall, dass nur ein Element diese Stelle füllen soll. Mehrere "gleichnamige" Sektionen einzufügen kann unübersichtlich bezüglich der Reihenfolge sein und wird darüber hinaus von unterschiedlichen Laravel-Versionen anders gehandhabt. In älteren Laravel-Versionen kann hier `@endsection` durch `@append` ersetzt werden, bei neueren (circa ab Version 7) kann mit `@parent` eine *Stelle* innerhalb der Section angegeben werden, wo durch Parent-Templates ggf. bereitgestellter Inhalt für diese Sektion angezeigt werden soll. Um PHP-Anweisungen (auch übergebene Variablen) in der View anzuzeigen, gibt es zwei verschiedene Möglichkeiten. Die häufiger verwendete schließt die PHP-Anweisung in doppelt geschweifte Klammern ein: `{{ ... }}` Hierbei wird ein Escaping von HTML-Zeichen vorgenommen, wodurch ggf. HTML-Quellcode ausgegeben wird. Dadurch können [XSS](https://de.wikipedia.org/wiki/Cross-Site-Scripting)-Attacken vermieden werden, vor allem, wenn der entsprechende Text durch User angegeben wird, aber wenn man "echtes" HTML ausgeben will, muss man [statt dessen diesen Code ausgeben mit `{!! ... !!}`](https://laravel.com/docs/8.x/blade#displaying-unescaped-data). Natürlich wird dringend davon abgeraten, das mit Text zu machen, der vom User übergeben wird. Will man Kommentare in die Blade-Datei einfügen, kann man sich normaler HTML-Kommentare bedienen, aber diese Kommentare finden sich dann in der generierten PHP-Datei wieder. Will man das nicht, so kann man auch Blade-Kommentare verwenden [mit folgender Syntax](https://laravel.com/docs/8.x/blade#comments): ``` {{-- Dies ist ein Kommentar und wird nicht in die PHP-Datei übergeben --}} ``` Befinden sich allerdings innerhalb des Kommentars noch Blade-Anweisungen (z.B. `{{ ... }}`), so würden diese immer noch ausgewertet, bevor erkannt wird, dass es sich um einen Kommentar handelt. Hierfür kann es nötig sein, der Blade Engine gezielt mitzuteilen, einen Ausdruck *nicht* auszuwerten. Dafür ersetzt man `{{ ... }}` durch `@{{ ... }}`. Das wird auch benötigt, falls [andere verwendete Frameworks diese Syntax verwenden](https://laravel.com/docs/8.x/blade#blade-and-javascript-frameworks) und Blade den Ausdruck nicht auswerten soll. Wenn Variablen an Blade übergeben werden, bei denen nicht klar ist, ob sie definiert sind (vor PHP 7.3 oder nicht mit `compact`), kann man innerhalb eines PHP-Ausdrucks mit Hilfe des `??`-Operators einen Standardwert festlegen. Soll ein ganzer Abschnitt vorhanden sein oder nicht, je nachdem, ob eine Variable belegt ist, verwendet man ``` @isset($var) ... @endisset ``` für einen Abschnitt, bei dem eine Variable belegt sein soll und ``` @empty($var) ... @endempty ``` für einen Abschnitt, bei dem eine Variable `undefined` sein soll. Bei einer Fallunterscheidung nimmt man beide Abschnitte, bei diesen Ausdrücken ist ein `@else` nicht vorgesehen, und diese Direktiven sind übersichtlicher als das allgemeinere ``` @if(...) ... @elseif(...) ... @else ... @endif ``` das es darüber hinaus noch gibt, vor allem, wenn in der Nähe davon diese allgemeineren Fallunterscheidungen zusätzlich noch verwendet werden. Auch in anderen Fällen gibt es [spezielle Direktiven](https://laravel.com/docs/8.x/blade#blade-directives), die deutlicher machen können, was der Autor beabsichtigt. Schließlich gibt es auch noch die Möglichkeit, mit der Direktive `@include('<View>')` eine komplette weitere View an einer Stelle einzubinden.