Laravel, Traits, Eventos em Models e Multi-tenancy

Além disso, a medida em que um sistema que utilizasse essa abordagem crescesse, seria cada vez mais chato estar sempre ‘lembrando’ de setar o tenant_id e de filtrar os registros recuperados do banco.

A verdade é que podemos tornar o dia-a-dia mais tranquilo, podemos nos permitir “esquecer” de tais detalhes de modo seguro, é possível delegar essa responsabilidade.

Filtrar os Models é questão de aplicar um escopo global a eles.

Setar o tenant_id sempre que um Model é criado pode ser feito através de um event listenner.

Discutimos ambas as abordagens no post anterior, então só nos resta colocar os conceitos em prática.

O escopo global TenantScopeAqui não há muito segredo, teríamos um escopo global como o mostrado abaixo<?phpnamespace AppScopes;use IlluminateDatabaseEloquentScope;use IlluminateDatabaseEloquentModel;use IlluminateDatabaseEloquentBuilder;use Auth;class TenantScope implements Scope{ public function apply(Builder $builder, Model $model) { if (Auth::check()) { $builder->where('tenant_id', Auth::user()->tenant_id); } }}Porém o diabo está nos detalhes… Como o nosso tenant_id está associado ao usuário realizando a requisição, temos de recuperar essa informação através do uso do Facade Auth.

Além disso temos de checar a autenticação antes de aplicar a query.

A necessidade dessa verificação é que podemos buscar registros através, por exemplo, de comandos, ou mesmo através do tinker.

Vale ressaltar que acima foi utilizada uma classe de escopo, mas poderia ter sido utilizada uma Closure, por exemplo.

O trait TenantScopedAgora podemos utilizar o escopo definido anteriormente e, junto a ele, adicionar um event listenner para setar o tenant_id .

Tudo isso em um Trait, como mostrado abaixo.

//.

use Auth;use AppTenantScope;use AppTenant;trait TenantScoped { protected static function bootTenantScoped() { static::creating(function (Model $model) { $model->tenant_id = Auth::user()->tenant_id; }); static::addGlobalScope(new TenantScope); } public function tenant() { return $this->belongsTo(Tenant::class); }}Agora, se quisermos que todo Model Foo seja particular a cada tenant, só precisamos usar o Trait, como mostra o exemplo abaixo.

// .

use AppTraitsTenantScoped;class Foo extends Model{ use TenantScoped; // .

}Se quisermos que o Model seja compartilhado por todos os Tenants, basta não usar o Trait.

Legal, né?Agora podemos lidar com os Models “escopados” como se pertencessem — salvo alguns casos que explicarei adiante — a bases diferentes, ou mesmo aplicações diferentes.

Garantimos o isolamento entre os tenants de maneira relativamente simples, fácil de lembrar e reutilizar.

Nem tudo são floresEmbora a implementação discutida permita que os Models da aplicação venham filtrados pelo escopo do tenant, há alguns casos especiais a serem levados em consideração.

Quando interagimos com a base de dados por métodos que operam diretamente no banco, ou que não utizam os Models para acessá-lo, não temos a garantia de que construções como escopos e event listenners sejam levados em consideração.

Basta tomar como exemplo o Trait SoftDeletes do Laravel.

Quando utilizando o Facade DB do framework, ele busca os registros sem levar em consideração o campo deleted_at.

Outro exemplo são as queries em regras de validação, que exibem comportamento semelhante.

Consequentemente, a solução mostrada possui alguns casos em que os detalhes devem ser lembrados e alguns cuidados tomados — não que isso não seja comum em outras soluções de outros problemas, de um modo geral.

Comentários do alémAcoplar a checagem do tenant_id ao acesso ao usuário autenticado, via Facade Auth, não cheira muito bem, no sentido de que podemos deixar essa chegagem mais transparente e encapsulada, sobretudo para permitir que o tenant possa ser configurado, por exemplo, em comandos ou testes.

Isso não é muito complicado.

Basta implementar uma classe que permita o set do tenant_id e a sua posterior recuperação.

Este tenant_id poderia ser configurado, em requisições, a partir de um middleware — que se comunicaria com uma instância da classe — e, em outros contextos, poderia ser configurado através de chamadas aos métodos da classe.

Naturalmente, como essa configuração do tenant_id é um estado global da aplicação, podemos implementar essa solução hipotética através de um singleton.

Os comentários acima visam apenas fomentar a curiosidade.

Apresentar como isso seria implementado foge um pouco do escopo deste post.

Além disso eu gostaria, antes, de discutir um pouco sobre conceitos como service providers, service container e Facades, que podem ser temas de um post futuro.

Por hoje é sóCom esse post busquei mostrar algumas coisas interessantes nas quais esbarrei enquanto navegando no mundo do Laravel.

É o primeiro que explora o que foi discutido em PHP, Traits, Laravel & Beyond.

Pretendo escrever alguns outros.

ReferênciasA maior parte do que foi discutido é retirado da documentação do Laravel e do PHP.

Naturalmente alguns posts me ajudaram e alimentaram minha curiosidade, os que me lembro são mostrados a seguir:What is a multi-tenant system?Joseph Jude Nov 17 '17 ・4 min readThis post was first posted on my blog It is no coincidence that designers of software…dev.

toDesmitificando Multitenancy — Parte 1: IntroduçãoA solução mais simples é geralmente correta.

medium.

comBooting Eloquent Model TraitsSimon Archer posted a new tutorial on booting Eloquent model traits: If you have a static function on your trait, named…laravel-news.

com.

. More details

Leave a Reply