"Si un ouvrier veut bien faire son travail, il doit d'abord affûter ses outils." - Confucius, "Les Entretiens de Confucius. Lu Linggong"
Page de garde > La programmation > Construire une application multi-tenant avec Honeystone/Context

Construire une application multi-tenant avec Honeystone/Context

Publié le 2024-08-18
Parcourir:260

À 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 ?

Exemple de projet

Pour notre exemple d'application, nous aurons une base d'utilisateurs mondiale organisée 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 puissent rejoindre plusieurs équipes, il s'agit donc d'une base d'utilisateurs globale.
Diagramme de base d'utilisateurs globale et de base d'utilisateurs locataire

Building a multi-tenant application with honeystone/context

En tant que SaaS, il est probable que l'équipe soit l'entité facturable (c'est-à-dire le siège) et que certains membres de l'équipe soient autorisés à gérer l'équipe. Je n'entrerai cependant pas dans les détails de cette implémentation dans cet exemple, mais j'espère qu'il fournira un contexte supplémentaire.

Installation

Pour garder cet article concis, je n'expliquerai pas comment démarrer votre projet Laravel. 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 utilisez simplement 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.

Les modèles

Commençons par configurer notre modèle d'équipe :

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.

D'accord, regardons donc notre projet :

belongsTo(Team::class);
    }
}

Un projet a un nom et appartient à une équipe.

Déterminer le contexte

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
Contexte d'équipe uniquement : 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 réelle, 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 :

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) :

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']);
    }
}

Il se passe un peu plus de choses ici.

La méthode definition() est chargée de définir le contexte en cours de résolution. L'équipe est obligatoire et doit être un modèle d'équipe, et le projet est accepté (c'est-à-dire facultatif) et doit être un modèle de projet (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é 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.

Maintenant que notre contexte d'application est défini, nous devons l'utiliser.

Accéder au contexte

Comme d'habitude, nous garderons les choses simples, même si elles sont un peu artificielles. Lorsque nous visualisons l'équipe, nous voulons voir une liste de projets. Nous pourrions créer notre TeamController pour gérer ces exigences comme ceci :

projects;

        return view('team', compact('projects'));
    }
}

Assez facile. Les projets appartenant au contexte d'équipe actuel sont transmis à notre vue. Imaginez que nous devions maintenant interroger des projets pour obtenir une vue plus spécialisée. Nous pourrions faire ceci :

id)
            ->where('name', 'like', "%$query%")
            ->get();

        return view('queried-projects', compact('projects'));
    }
}

Cela devient un peu compliqué maintenant, et il est beaucoup trop facile d'oublier accidentellement de « définir » la requête par équipe. Nous pouvons résoudre ce problème en utilisant le trait BelongsToContext sur notre modèle de projet :

belongsTo(Team::class);
    }
}

Toutes les requêtes de projet seront désormais récupérées par le contexte de l'équipe et le modèle d'équipe actuel sera automatiquement injecté dans les nouveaux modèles de projet.

Simplifions ce contrôleur :

get();

        return view('queried-projects', compact('projects'));
    }
}

C'est tout les amis

À partir de là, vous ne faites que créer votre application. Le contexte est facilement accessible, vos requêtes sont limitées et les tâches en file d'attente auront automatiquement accès au même contexte à partir duquel elles ont été envoyées.

Tous les problèmes liés au contexte ne sont cependant pas résolus. Vous souhaiterez probablement créer des macros de validation pour donner un peu de contexte à vos règles de validation, et n'oubliez pas que les requêtes manuelles ne verront pas le contexte automatiquement appliqué.

Si vous envisagez d'utiliser ce package dans votre prochain projet, nous serions ravis de vous entendre. Les commentaires et les contributions sont toujours les bienvenus.

Vous pouvez consulter le référentiel GitHub pour obtenir une documentation supplémentaire. Si vous trouvez notre package utile, veuillez laisser une étoile.

Jusqu'à la prochaine fois..


Cet article a été initialement publié sur le blog Honeystone. Si vous aimez nos articles, pensez à consulter davantage notre contenu là-bas.

Déclaration de sortie Cet article est reproduit sur : https://dev.to/piranhageorge/building-a-multi-tenant-application-with-honeystonecontext-3eob?1 En cas d'infraction, veuillez contacter [email protected] pour le supprimer.
Dernier tutoriel Plus>

Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.

Copyright© 2022 湘ICP备2022001581号-3