Heim >Backend-Entwicklung >PHP-Tutorial >Erstellen einer mandantenfähigen Anwendung mit Honeystone/Kontext
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 werden wir eine globale Benutzerbasis haben in Teams organisiert und jedes Team wird mehrere Projekte haben. 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 dies tun in der Lage sein, mehreren Teams beizutreten, also eine globale Benutzerbasis.
Globale Benutzerbasis vs. Mandanten-Benutzerbasis-Diagramm
Als SaaS ist das wahrscheinlich Das Team wäre die gebührenpflichtige Einheit (d. h. der Sitz) und bestimmten Teammitgliedern würde die Erlaubnis erteilt, das Team zu leiten. Ich werde in diesem Beispiel jedoch nicht auf diese Implementierungsdetails eingehen, aber ich hoffe, dass es zusätzlichen Kontext liefert.
Um diesen Beitrag prägnant zu halten, werde ich nicht erklären, wie Sie Ihr Laravel starten Projekt. 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 entsprechend verwenden.
Beginnen wir mit der Konfiguration unseres Teammodells:
<?php declare(strict_types=1); namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; class Team extends Model { protected $fillable = ['name']; public function members(): BelongsToMany { return $this->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:
<?php declare(strict_types=1); namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Project extends Model { protected $fillable = ['name']; public function team(): BelongsTo { return $this->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:
<?php declare(strict_types=1); namespace App\Http\Middleware; use function Honestone\Context\context; class TeamContextMiddleware { public function handle(Request $request, Closure $next): mixed { //pull the team parameter from the route $teamId = $request->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 greifen wir auf die Team-ID aus der Route zurück 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 mit unserem AppResolver und übergeben dabei unsere Team-ID und unsere Projekt-ID (oder null):
<?php declare(strict_types=1); namespace App\Context\Resolvers; use App\Models\Team; use Honeystone\Context\ContextResolver; use Honeystone\Context\Contracts\DefinesContext; use function Honestone\Context\context; class AppResolver extends ContextResolver { public function __construct( private readonly int $teamId, private readonly ?int $projectId = null, ) {} public function define(DefinesContext $definition): void { $definition ->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 geht es noch ein bisschen weiter.
Das define( )-Methode 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 in der Definition nicht erforderlich ist.
Nach der Auflösung des Teams und des Projekts ruft der ContextInitializer die optionalen Methoden checkTeam() und checkProject() auf. Diese Methoden führen Integritätsprüfungen durch. Für checkTeam() stellen wir sicher, dass der authentifizierte Benutzer Mitglied des Teams ist, und für 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.
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:
<?php declare(strict_types=1); namespace App\Http\Controllers; use Illuminate\View\View; use function compact; use function Honestone\Context\context; use function view; class TeamController { public function __invoke(Request $request): View { $projects = context('team')->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:
<?php declare(strict_types=1); namespace App\Http\Controllers; use Illuminate\View\View; use function compact; use function Honestone\Context\context; use function view; class ProjectQueryController { public function __invoke(Request $request, string $query): View { $projects = Project::where('team_id', context('team')->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:
<?php declare(strict_types=1); namespace App\Models; use Honeystone\Context\Models\Concerns\BelongsToContext; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Project extends Model { use BelongsToContext; protected static array $context = ['team']; protected $fillable = ['name']; public function team(): BelongsTo { return $this->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:
<?php declare(strict_types=1); namespace App\Http\Controllers; use Illuminate\View\View; use function compact; use function view; class ProjectQueryController { public function __invoke(Request $request, string $query): View { $projects = Project::where('name', 'like', "%$query%")->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!