Não deve ser confundido com a nova biblioteca de contexto do Laravel, este pacote pode ser usado para construir aplicativos multi-contexto e multi-tenant. A maioria das bibliotecas multilocatários tem essencialmente um único contexto de “inquilino”, portanto, se você precisar de vários contextos, as coisas podem ficar um pouco complicadas. Este novo pacote resolve esse problema.
Vamos ver um exemplo, certo?
Para nosso aplicativo de exemplo, teremos uma base de usuários global organizada em equipes e cada equipe terá vários projetos. Esta é uma estrutura bastante comum em muitos aplicativos de software como serviço.
Não é incomum que aplicativos multilocatários tenham cada base de usuários dentro de um contexto de locatário, mas para nosso aplicativo de exemplo, queremos que os usuários possam ingressar em várias equipes, portanto, é uma base de usuários global.
Diagrama de base de usuários global versus base de usuários de locatários
Como um SaaS, é provável que a equipe seja a entidade faturável (ou seja, o assento) e certos membros da equipe recebam permissão para gerenciar a equipe. Não vou me aprofundar nesses detalhes de implementação neste exemplo, mas espero que ele forneça algum contexto adicional.
Para manter este post conciso não vou explicar como iniciar seu projeto Laravel. Já existem muitos recursos melhores disponíveis para isso, inclusive a documentação oficial. vamos supor que você já tenha um projeto Laravel, com modelos de Usuário, Equipe e Projeto, e esteja pronto para começar a implementar nosso pacote de contexto.
A instalação é um simples elogio do compositor:
composer install honeystone/context
Esta biblioteca tem uma função de conveniência, context(), que a partir do Laravel 11 entra em conflito com a própria função de contexto do Laravel. Isso não é realmente um problema. Você pode importar nossa função:
use function Honestone\Context\context;
Ou apenas use o contêiner de injeção de dependência do Laravel. Ao longo deste post, presumirei que você importou a função e a usará de acordo.
Vamos começar configurando nosso modelo de equipe:
belongsToMany(User::class); } public function projects(): HasMany { return $this->hasMany(Project::class); } }
Uma equipe tem nome, membros e projetos. Dentro do nosso aplicativo, apenas membros de uma equipe poderão acessar a equipe ou seus projetos.
Ok, então vamos dar uma olhada em nosso projeto:
belongsTo(Team::class); } }
Um projeto tem um nome e pertence a uma equipe.
Quando alguém acessa nosso aplicativo, precisamos determinar em qual equipe e projeto ele está trabalhando. Para simplificar, vamos lidar com isso com parâmetros de rota. Também assumiremos que apenas usuários autenticados podem acessar o aplicativo.
Nem contexto de equipe nem de projeto: app.mysaas.dev
Somente contexto de equipe: app.mysaas.dev/my-team
Contexto da equipe e do projeto: app.mysaas.dev/my-team/my-project
Nossas rotas serão mais ou menos assim:
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); }); });
Esta é uma abordagem muito inflexível, dado o potencial para conflitos de namespace, mas mantém o exemplo conciso. Em uma aplicação do mundo real, você desejará lidar com isso de maneira um pouco diferente, talvez anothersaas.dev/teams/my-team/projects/my-project ou my-team.anothersas.dev/projects/my-project.
Devemos examinar nosso AppContextMiddleware primeiro. Este middleware inicializa o contexto da equipe e, se definido, o contexto do projeto:
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)); } }
Para começar, pegamos o ID da equipe da rota e depois esquecemos o parâmetro da rota. Não precisamos que o parâmetro chegue aos nossos controladores quando estiver no contexto. Se um ID de projeto for definido, nós o extraímos também. Em seguida, inicializamos o contexto usando nosso AppResolver passando nosso ID de equipe e nosso ID de projeto (ou nulo):
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']); } }
Um pouco mais acontecendo aqui.
O método define() é responsável por definir o contexto que está sendo resolvido. A equipe é obrigatória e deve ser um modelo de Equipe, e o projeto é aceito (ou seja, opcional) e deve ser um modelo de Projeto (ou nulo).
resolveTeam() será chamado internamente na inicialização. Ele retorna a equipe ou nulo. No caso de uma resposta nula, CouldNotResolveRequiredContextException será lançada pelo ContextInitializer.
resolveProject() também será chamado internamente na inicialização. Retorna o Projeto ou nulo. Neste caso, uma resposta nula não resultará em uma exceção, pois o projeto não é exigido pela definição.
Depois de resolver a equipe e o projeto, o ContextInitializer chamará os métodos opcionais checkTeam() e checkProject(). Esses métodos realizam verificações de integridade. Para checkTeam() garantimos que o usuário autenticado é membro da equipe, e para checkProject() verificamos se o projeto pertence à equipe.
Finalmente, todo resolvedor precisa de um método deserialization(). Este método é usado para restabelecer um contexto serializado. Mais notavelmente, isso acontece quando o contexto é usado em um trabalho na fila.
Agora que nosso contexto de aplicação está definido, devemos usá-lo.
Como sempre, manteremos tudo simples, embora um pouco artificial. Ao visualizar a equipe queremos ver uma lista de projetos. Poderíamos construir nosso TeamController para lidar com esses requisitos como este:
projects; return view('team', compact('projects')); } }
Bastante fácil. Os projetos pertencentes ao contexto atual da equipe são passados para nossa visão. Imagine que agora precisamos consultar projetos para obter uma visão mais especializada. Poderíamos fazer isso:
id) ->where('name', 'like', "%$query%") ->get(); return view('queried-projects', compact('projects')); } }
Está ficando um pouco complicado agora e é muito fácil esquecer acidentalmente de 'definir o escopo' da consulta por equipe. Podemos resolver isso usando a característica BelongsToContext em nosso modelo de projeto:
belongsTo(Team::class); } }
Todas as consultas do projeto agora serão coletadas pelo contexto da equipe e o modelo de equipe atual será automaticamente injetado em novos modelos de projeto.
Vamos simplificar esse controlador:
get(); return view('queried-projects', compact('projects')); } }
A partir daqui, você está apenas construindo seu aplicativo. O contexto está facilmente disponível, suas consultas têm escopo definido e os trabalhos na fila terão acesso automático ao mesmo contexto do qual foram despachados.
Nem todos os problemas relacionados ao contexto são resolvidos. Você provavelmente desejará criar algumas macros de validação para dar um pouco de contexto às suas regras de validação, e não se esqueça que as consultas manuais não terão o contexto aplicado automaticamente.
Se você planeja usar este pacote em seu próximo projeto, adoraríamos ouvir sua opinião. Feedback e contribuição são sempre bem-vindos.
Você pode verificar o repositório GitHub para obter documentação adicional. Se você achar nosso pacote útil, deixe uma estrela.
Até a próxima..
Este artigo foi postado originalmente no Honeystone Blog. Se você gosta de nossos artigos, considere conferir mais conteúdos por lá.
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3