Nicht zu verwechseln mit der neuen Kontextbibliothek von Laravel, dieses Paket kann zum Erstellen von Multi-Kontext-Multi-Tenant-Anwendungen verwendet werden. Die meisten mandantenfähigen Bibliotheken verfügen im Wesentlichen über einen einzigen „Mandanten“-Kontext. Wenn Sie also mehrere Kontexte benötigen, kann es etwas umständlich werden. Dieses neue Paket löst dieses Problem.
Schauen wir uns ein Beispiel an, oder?
Für unsere Beispielanwendung verfügen wir über eine globale Benutzerbasis, die in Teams organisiert ist und jedes Team mehrere Projekte hat. Dies ist eine recht häufige Struktur in vielen Software-as-a-Service-Anwendungen.
Es ist nicht ungewöhnlich, dass bei mandantenfähigen Anwendungen jede Benutzerbasis in einem Mandantenkontext existiert, aber für unsere Beispielanwendung möchten wir, dass Benutzer mehreren Teams beitreten können, also eine globale Benutzerbasis.
Diagramm „Globale Benutzerbasis vs. Mandantenbenutzerbasis“
Als SaaS ist es wahrscheinlich, dass das Team die abrechenbare Einheit (d. h. der Sitz) ist und bestimmten Teammitgliedern die Erlaubnis erteilt wird, das Team zu verwalten. Ich werde in diesem Beispiel zwar nicht näher auf diese Implementierungsdetails eingehen, aber es bietet hoffentlich etwas zusätzlichen Kontext.
Um diesen Beitrag prägnant zu halten, werde ich nicht erklären, wie Sie Ihr Laravel-Projekt starten. Dafür stehen bereits viele bessere Ressourcen zur Verfügung, nicht zuletzt die offizielle Dokumentation. Nehmen wir einfach an, Sie haben bereits ein Laravel-Projekt mit Benutzer-, Team- und Projektmodellen und sind bereit, mit der Implementierung unseres Kontextpakets zu beginnen.
Die Installation ist ein einfacher Composer-Befehl:
composer install honeystone/context
Diese Bibliothek verfügt über eine praktische Funktion, context(), die ab Laravel 11 mit der eigenen Kontextfunktion von Laravel kollidiert. Das ist eigentlich kein Problem. Sie können entweder unsere Funktion importieren:
use function Honestone\Context\context;
Oder verwenden Sie einfach den Dependency-Injection-Container von Laravel. In diesem Beitrag gehe ich davon aus, dass Sie die Funktion importiert haben und sie entsprechend verwenden.
Beginnen wir mit der Konfiguration unseres Teammodells:
belongsToMany(User::class); } public function projects(): HasMany { return $this->hasMany(Project::class); } }
Ein Team hat einen Namen, Mitglieder und Projekte. Innerhalb unserer Anwendung können nur Mitglieder eines Teams auf das Team oder seine Projekte zugreifen.
Okay, schauen wir uns also unser Projekt an:
belongsTo(Team::class); } }
Ein Projekt hat einen Namen und gehört zu einem Team.
Wenn jemand auf unsere Anwendung zugreift, müssen wir feststellen, in welchem Team und Projekt er arbeitet. Der Einfachheit halber behandeln wir dies mit Routenparametern. Wir gehen außerdem davon aus, dass nur authentifizierte Benutzer auf die Anwendung zugreifen können.
Weder Team noch Projektkontext: app.mysaas.dev
Nur Teamkontext: app.mysaas.dev/my-team
Team- und Projektkontext: app.mysaas.dev/my-team/my-project
Unsere Routen werden in etwa so aussehen:
Route::middleware('auth')->group(function () { Route::get('/', DashboardController::class); Route::middleware(AppContextMiddleware::Class)->group(function () { Route::get('/{team}', TeamController::class); Route::get('/{team}/{project}', ProjectController::class); }); });
Angesichts der Möglichkeit von Namespace-Konflikten ist dies ein sehr unflexibler Ansatz, aber er hält das Beispiel prägnant. In einer realen Anwendung möchten Sie dies etwas anders handhaben, vielleicht anothersaas.dev/teams/my-team/projects/my-project oder my-team.anothersas.dev/projects/my-project.
Wir sollten uns zuerst unsere AppContextMiddleware ansehen. Diese Middleware initialisiert den Teamkontext und, falls festgelegt, den Projektkontext:
route('team'); $request->route()->forgetParameter('team'); $projectId = null; //if there's a project, pull that too if ($request->route()->hasParamater('project')) { $projectId = $request->route('project'); $request->route()->forgetParameter('project'); } //initialise the context context()->initialize(new AppResolver($teamId, $projectId)); } }
Zunächst holen wir uns die Team-ID aus der Route und vergessen dann den Routenparameter. Wir brauchen nicht, dass der Parameter unsere Controller erreicht, sobald er im Kontext ist. Wenn eine Projekt-ID festgelegt ist, ziehen wir diese ebenfalls. Anschließend initialisieren wir den Kontext mithilfe unseres AppResolvers und übergeben dabei unsere Team-ID und unsere Projekt-ID (oder null):
require('team', Team::class) ->accept('project', Project::class); } public function resolveTeam(): ?Team { return Team::with('members')->find($this->teamId); } public function resolveProject(): ?Project { return $this->projectId ?: Project::with('team')->find($this->projectId); } public function checkTeam(DefinesContext $definition, Team $team): bool { return $team->members->find(context()->auth()->getUser()) !== null; } public function checkProject(DefinesContext $definition, ?Project $project): bool { return $project === null || $project->team->id === $this->teamId; } public function deserialize(array $data): self { return new static($data['team'], $data['project']); } }
Hier ist noch ein bisschen mehr los.
Die Methode define() ist für die Definition des aufzulösenden Kontexts verantwortlich. Das Team ist erforderlich und muss ein Teammodell sein, und das Projekt wird akzeptiert (d. h. optional) und muss ein Projektmodell (oder null) sein.
resolveTeam() wird bei der Initialisierung intern aufgerufen. Es gibt das Team oder null zurück. Im Falle einer Nullantwort wird die CouldNotResolveRequiredContextException vom ContextInitializer ausgelöst.
resolveProject() wird bei der Initialisierung auch intern aufgerufen. Es gibt das Projekt oder null zurück. In diesem Fall führt eine Nullantwort nicht zu einer Ausnahme, da das Projekt gemäß der Definition nicht erforderlich ist.
Nachdem das Team und das Projekt aufgelöst wurden, ruft der ContextInitializer die optionalen Methoden checkTeam() und checkProject() auf. Diese Methoden führen Integritätsprüfungen durch. Bei checkTeam() stellen wir sicher, dass der authentifizierte Benutzer Mitglied des Teams ist, und bei checkProject() prüfen wir, ob das Projekt zum Team gehört.
Schließlich benötigt jeder Resolver eine Deserialization()-Methode. Diese Methode wird verwendet, um einen serialisierten Kontext wiederherzustellen. Dies geschieht insbesondere dann, wenn der Kontext in einem Job in der Warteschlange verwendet wird.
Da nun unser Anwendungskontext festgelegt ist, sollten wir ihn verwenden.
Wie üblich halten wir es einfach, wenn auch etwas gekünstelt. Beim Anzeigen des Teams möchten wir eine Liste der Projekte sehen. Wir könnten unseren TeamController so aufbauen, dass er diese Anforderungen erfüllt:
projects; return view('team', compact('projects')); } }
Einfach genug. Die zum aktuellen Teamkontext gehörenden Projekte werden unserer Sicht übergeben. Stellen Sie sich vor, wir müssen jetzt Projekte abfragen, um eine speziellere Ansicht zu erhalten. Wir könnten Folgendes tun:
id) ->where('name', 'like', "%$query%") ->get(); return view('queried-projects', compact('projects')); } }
Jetzt wird es etwas fummelig und es passiert viel zu leicht, dass man versehentlich vergisst, die Abfrage nach Team zu „eingrenzen“. Wir können dieses Problem mithilfe der Eigenschaft „BelongsToContext“ in unserem Projektmodell lösen:
belongsTo(Team::class); } }
Alle Projektabfragen werden nun vom Teamkontext erfasst und das aktuelle Teammodell wird automatisch in neue Projektmodelle eingefügt.
Lassen Sie uns diesen Controller vereinfachen:
get(); return view('queried-projects', compact('projects')); } }
Von hier an erstellen Sie nur noch Ihre Anwendung. Der Kontext ist leicht zugänglich, Ihre Abfragen haben einen Gültigkeitsbereich und Aufträge in der Warteschlange haben automatisch Zugriff auf denselben Kontext, aus dem sie gesendet wurden.
Es werden jedoch nicht alle kontextbezogenen Probleme gelöst. Sie möchten wahrscheinlich einige Validierungsmakros erstellen, um Ihren Validierungsregeln ein wenig Kontext zu geben, und vergessen Sie nicht, dass bei manuellen Abfragen der Kontext nicht automatisch angewendet wird.
Wenn Sie planen, dieses Paket in Ihrem nächsten Projekt zu verwenden, würden wir uns freuen, von Ihnen zu hören. Feedback und Beiträge sind immer willkommen.
Weitere Dokumentation finden Sie im GitHub-Repository. Wenn Sie unser Paket nützlich finden, hinterlassen Sie bitte einen Stern.
Bis zum nächsten Mal..
Dieser Artikel wurde ursprünglich im Honeystone-Blog veröffentlicht. Wenn Ihnen unsere Artikel gefallen, schauen Sie sich dort noch weitere Inhalte an.
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3