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 vorhanden ist, 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 einfaches Composer-Kommentar:
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.
Finally, every resolver needs a deserialization() method. This method is used to reinstate a serialised context. Most notably this happens when the context is used in a queued job.
Now that our application context is set, we should use it.
As usual, we’ll keep it simple, if a little contrived. When viewing the team we want to see a list of projects. We could build our TeamController to handle this requirements like this:
projects; return view('team', compact('projects')); } }
Easy enough. The projects belonging to the current team context are passed to our view. Imagine we now need to query projects for a more specialised view. We could do this:
id) ->where('name', 'like', "%$query%") ->get(); return view('queried-projects', compact('projects')); } }
It’s getting a little fiddly now, and it’s far too easy to accidentally forget to ‘scope’ the query by team. We can solve this using the BelongsToContext trait on our Project model:
belongsTo(Team::class); } }
All project queries will now be scooped by the team context and the current Team model will be automatically injected into new Project models.
Let’s simplify that controller:
get(); return view('queried-projects', compact('projects')); } }
From here onwards, you’re just building your application. The context is easily at hand, your queries are scoped and queued jobs will automagically have access to the same context from which they were dispatched.
Not all context related problems are solved though. You’ll probably want to create some validation macros to give your validation rules a little context, and don’t forget manual queries will not have the context automatically applied.
If you’re planning to use this package in your next project, we’d love to hear from you. Feedback and contribution is always welcome.
You can checkout the GitHub repository for additional documentation. If you find our package useful, please drop a star.
Until next time..
This article was originally posted to the Honeystone Blog. If you like our articles, consider checking our more of our content over there.
Das obige ist der detaillierte Inhalt vonErstellen einer mandantenfähigen Anwendung mit Honeystone/Kontext. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!