| 935 Wörter
Ahoi,
ich sah mich zuletzt vor der Herausforderung eine interne Firmenanwendung mehrmandanten-fähig zu gestalten. Voraus geht dem Ganzen, dass alle Mandanten sich, dasselbe Daten-Layout teilen aber jeder Mandant jeweils eine eigene Datenbank führt.
In diesem Fall ist der ganze Prozess relativ einfach umzusetzen. Es ist auch möglich, vollkommen unterschiedliche Datenquellen (Datenbanken, APIs etc) zu vereinheitlichen, aber vor dem Problem stand ich zum Glück nicht :)
Zuallererst ist uns also bewusst, dass wir praktisch immer mit denselben Tabellen und Abfragen arbeiten können, unabhängig davon in welchem Mandaten man sich bewegt. Es muss “lediglich” festgestellt werden, welcher Mandant gerade gefragt ist, um dann die korrekte Datenbank abzufragen. Aber wie?
Mein konkreter Fall bezieht sich auf eine Web-Anwendung, welche die Daten aus allen Mandaten zusammenführen soll, ohne das drei eigenständige Anwendungen entwickelt werden müssen.
Fangen wir also mit einem normalen Laravel Projekt an:
|
|
Die Ordnerstruktur sieht wie folgt aus:
|
|
Damit wir die allgemeine Logik von tenant-spezifischer unterscheiden können, legen wir im Wurzelordner den Ordner " tenants" und “tenants/Models” an:
|
|
Wieso aber legen wir einfach nur den Ordner “Tenants” unter “app/Models” an?
Grundsätzlich funktioniert das natürlich aber es kann in diesem Modell zu unübersichtlich werden, wenn neben tenant-spezifischen Models auch Controller, Middlewares, Service Provider, Scopes usw. benötigt werden. Dann müsste nämlich ebenfalls unter “app/Providers”, “app/Http/Controllers”, “app/Http/Middlewares” der Ordner “Tenants” angelegt werden.
Ich bevorzuge daher einen einzigen Ordner, welcher durch seine Struktur den “app” Ordner von Laravel widerspiegelt. Das würde dann in etwa so aussehen:
|
|
Damit die gesamte Organisation des Codes einheitlich ist, geben wir uns selbst zwei Einschränkungen:
Eine allgemeingültige Implementierung ist IMMER eine abstrakte Klasse im Unterordner “app”
Eine tenant-spezifische Implementierung muss IMMER von einer allgemeingültigen erben, liegt im Unterordner ' tenants/TENANT_NAME/' und folgt der Ordnersturkur der allgemeingültigen Implementierung.
Diese Einschränkungen erlauben uns, den Code vom korrekten Tenant zur Laufzeit zu bestimmen.
Als Nächstes müssen wir den Autoloader mit den neuen “tenants” Ordner bekannt machen. Dazu bearbeiten wir die composer.json und fügen Folgendes im Block “autoload” ein:
|
|
Kommen wir jetzt aber zu Code:
Um die Funktionsweise zu demonstrieren, benötigen wir 3 Models. Eins als allgemeine Implementierung und eins für jeden Tenant. Zur Vereinfachung nutzen wir das “User” Model, welches Laravel mitliefert.
|
|
|
|
Die Model jedes Tenants erben das “User” Model und überschreiben lediglich die Datenbank-Verbindung. Diese wären in der Praxis noch in der “config/database.php” anzulegen.
Im Anschluss müssen wir einmal den Autoloader ausführen, um die neuen Klassen zu finden:
|
|
Und wenn in der web.php Folgendes einfügen, können wir uns ein ersten Bild von der aktuellen Situation machen:
|
|
Ausgabe:
App\Models\User {#286 ▼ // routes/web.php:17 #connection: null … }
Tenants\Tenant1\Models\User {#287 ▼ // routes/web.php:17 #connection: “tenant1” … }
Tenants\Tenant2\Models\User {#288 ▼ // routes/web.php:17 #connection: “tenant2” … }
Wie zu erwarten, bekommen wir genau das, wo nach wir gefragt haben.
Aber wieso nutzen wir die resolve Funktion?
Laravels resolve Funktion erlaubt es uns, den Autoloader zu manipulieren und anhand von Laufzeitvariablen zu bestimmen, welcher Tenant zum aktuellen Zeitpunkt gefordert wird.
In unserem Fall können wir aber keine Interfaces einsetzen, da einiges in der Konfiguration und Authentifizierungs-Logik von der “User” Klasse abhängt. Damit wir das umgehen, nutzen wir Laravels Binding und binden das User Model zur Laufzeit auf einen Tenant:
|
|
Wir sagen Laravel also, wenn nach der Klasse “\App\Models\User” gefragt wird, erzeuge ein Objekt der Klasse " \Tenants\Tenant1\Models\User" und gebe es zurück.
Ausgabe:
App\Models\User {#286 ▼ // routes/web.php:17 #connection: “tenant1” … }
Tenants\Tenant1\Models\User {#287 ▼ // routes/web.php:17 #connection: “tenant1” … }
Tenants\Tenant2\Models\User {#288 ▼ // routes/web.php:17 #connection: “tenant2” … }
Für eine komplexere Logik kann man auch eine Funktion übergeben:
|
|
Ausgabe:
App\Models\User {#286 ▼ // routes/web.php:17 #connection: “tenant2” … }
Tenants\Tenant1\Models\User {#287 ▼ // routes/web.php:17 #connection: “tenant1” … }
Tenants\Tenant2\Models\User {#288 ▼ // routes/web.php:17 #connection: “tenant2” … }
Wunderbar! Wir können nun die Logik also an unsere Bedürfnisse anpassen und müssen im Anschluss nur noch alle Aufrufe der Klasse “App\Models\User” mit der resolve Funktion wrappen :)