Maison > Article > développement back-end > Construire une application multi-tenant avec Honeystone/Context
À ne pas confondre avec la nouvelle bibliothèque de contexte de Laravel, ce package peut être utilisé pour créer des applications multi-contextes multi-locataires. La plupart des bibliothèques multi-locataires ont essentiellement un seul contexte « locataire », donc si vous avez besoin de plusieurs contextes, les choses peuvent devenir un peu délicates. Ce nouveau package résout ce problème.
Regardons un exemple, d'accord ?
Pour notre exemple d'application, nous aurons une base d'utilisateurs globale qui est organisé en équipes et chaque équipe aura plusieurs projets. Il s'agit d'une structure assez courante dans de nombreuses applications Software as a Service.
Il n'est pas rare que les applications multi-locataires aient chaque base d'utilisateurs existant dans un contexte de locataire, mais pour notre exemple d'application, nous voulons que les utilisateurs être capable de rejoindre plusieurs équipes, il s'agit donc d'une base d'utilisateurs globale.
Diagramme de la base d'utilisateurs globale par rapport à la base d'utilisateurs locataire
En tant que SaaS, il est probable que l'équipe serait l'entité facturable (c'est-à-dire le siège) et certains membres de l'équipe recevraient l'autorisation de gérer l'équipe. Cependant, je n'entrerai pas dans ces détails d'implémentation dans cet exemple, mais j'espère que cela fournira un contexte supplémentaire.
Pour que cet article reste concis, je n'expliquerai pas comment démarrer votre Laravel projet. Il existe déjà de nombreuses meilleures ressources disponibles pour cela, notamment la documentation officielle. supposons simplement que vous avez déjà un projet Laravel, avec des modèles d'utilisateur, d'équipe et de projet, et que vous êtes prêt à commencer à implémenter notre package de contexte.
L'installation est une simple recommandation du compositeur :
composer install honeystone/context
Cette bibliothèque a une fonction pratique, context(), qui, à partir de Laravel 11, entre en conflit avec la propre fonction contextuelle de Laravel. Ce n'est pas vraiment un problème. Vous pouvez soit importer notre fonction :
use function Honestone\Context\context;
Ou simplement utiliser le conteneur d'injection de dépendances de Laravel. Tout au long de cet article, je supposerai que vous avez importé la fonction et que vous l'utilisez en conséquence.
Commençons par configurer notre modèle Team :
<?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); } }
Une équipe a un nom, des membres et des projets. Au sein de notre application, seuls les membres d'une équipe pourront accéder à l'équipe ou à ses projets.
Bon, regardons donc notre Projet :
<?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); } }
Un projet a un nom et appartient à une équipe.
Lorsque quelqu'un accède à notre application, nous devons déterminer dans quelle équipe et dans quel projet il travaille. Pour simplifier les choses, traitons cela avec les paramètres d'itinéraire. Nous supposerons également que seuls les utilisateurs authentifiés peuvent accéder à l'application.
Ni contexte d'équipe ni de projet : app.mysaas.dev
Uniquement contexte d'équipe : app.mysaas.dev/my-team
Contexte de l'équipe et du projet : app.mysaas.dev/my-team/my-project
Nos itinéraires ressembleront à ceci :
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); }); });
Il s'agit d'une approche très rigide, étant donné le potentiel de conflits d'espaces de noms, mais elle permet de garder l'exemple concis. Dans une application du monde réel, vous souhaiterez gérer cela un peu différemment, peut-être anothersaas.dev/teams/my-team/projects/my-project ou my-team.anothersas.dev/projects/my-project.
Nous devrions d'abord examiner notre AppContextMiddleware. Ce middleware initialise le contexte de l'équipe et, s'il est défini, le contexte du projet :
<?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)); } }
Pour commencer, nous récupérons l'identifiant de l'équipe de la route, puis oublions le paramètre de route. Nous n’avons pas besoin que le paramètre atteigne nos contrôleurs une fois qu’il est dans le contexte. Si un identifiant de projet est défini, nous le extrayons également. Nous initialisons ensuite le contexte à l'aide de notre AppResolver en passant notre identifiant d'équipe et notre identifiant de projet (ou 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']); } }
Un peu plus de choses ici.
La définition( ) est chargée de définir le contexte en cours de résolution. L'équipe est obligatoire et doit être un modèle Team, et le projet est accepté (c'est-à-dire facultatif) et doit être un modèle Project (ou nul).
resolveTeam() sera appelé en interne lors de l'initialisation. Il renvoie l'équipe ou null. En cas de réponse nulle, l'exception CouldNotResolveRequiredContextException sera levée par ContextInitializer.
resolveProject() sera également appelée en interne lors de l'initialisation. Il renvoie le projet ou null. Dans ce cas, une réponse nulle n'entraînera pas d'exception car le projet n'est pas requis par la définition.
Après avoir résolu l'équipe et le projet, ContextInitializer appellera les méthodes facultatives checkTeam() et checkProject(). Ces méthodes effectuent des contrôles d'intégrité. Pour checkTeam() nous nous assurons que l'utilisateur authentifié est membre de l'équipe, et pour checkProject() nous vérifions que le projet appartient à l'équipe.
Enfin, chaque résolveur a besoin d'une méthode de désérialisation(). Cette méthode est utilisée pour rétablir un contexte sérialisé. Cela se produit notamment lorsque le contexte est utilisé dans une tâche en file d'attente.
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.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!