Olá dev, hoje o assunto vai ser um pouco mais longo, vamos conversar um pouco sobre multi usuários no Laravel, essa arquitetura é chamada de multi tenant, ou multi tenancy.
Tenant vem de hóspede, pois cada usuário é equivalente a um hóspede nessa arquitetura.
Tipos de multi tenant
Segundo meus estudos, existem três principais formas para aplicar essa arquitetura em seu projeto:
Por mais dos diversos lugares que eu li e vi, geralmente se resumia nessas três abordagens.
1-Com banco de dados separado: em alguns casos, podemos ter até mesmo uma instância inteira da aplicação por cliente. Isso seria o mais seguro, se considerar o isolamento dos dados, porém acaba que a manutenção, o gerenciamento e o custo dessa abordagem tornam-se caras e difíceis.
2-Uma única instância de banco com esquemas separados por cliente: nessa abordagem para o meu uso, na minha aplicação (vou detalhar mais a frente), não parecia ser muito viável, pois para cada novo usuário, eu teria que fazer um create database tenantX.
3-Um único banco de dados para todos os tenants: essa foi a abordagem que escolhi. Nessa modalidade, é feito o vinculo dos usuários por um id, uma "chave de locação". Aqui, assim como nas outras abordagens, um usuário não vê os dados dos outros usuários, obviamente. A não ser que você defina um mesmo tenant_id em outro usuário.
Sobre minha aplicação
Antes de irmos para o código, vamos bater mais um papo, sobre a minha aplicação.
No momento em que escrevo, minha aplicação está desenvolvida com o backend em Laravel e o frontend em Vue.Js. A comunicação do Laravel com o Vue está totalmente sendo feita por API, ou seja, o backend está bem separado do frontend.
Para fazer a autenticação (Login) na aplicação, estou usando JTW, no momento que o response do login é feito para o frontend, o token e usuário é salvo no gerenciador de estado do Vue, o Pinia.
Isso garante que caso o usuário troque os dados no localStorage do navegador, não afete a aplicação, acessando dados que não deve.
Ainda não terei tela de registro, por questões de que ainda não quero liberar a aplicação para todos usarem, então os usuários vão ser inseridos manualmente no banco.
Sem mais enrolação, vamos ao que interessa…
Preparando o Banco
Neste passo vamos ter que criar três migrations:
1-Criando a tabela de tenants:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->nullable()->index();
$table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tenants');
}
};
2-Alterar a tabela de usuário para ter o tenant_id:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function(Blueprint $table) {
$table->unsignedBigInteger('tenant_id')->nullable()->after('name')->index();
$table->foreign('tenant_id')->references('id')->on('tenants');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function($table) {
$table->dropColumn('tenant_id');
});
}
};
3-Alterar demais tabelas para ter o tenant_id:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('tabela_1', function(Blueprint $table) {
$table->unsignedBigInteger('tenant_id')->nullable()->after('description')->index();
$table->foreign('tenant_id')->references('id')->on('tenants');
});
Schema::table('tabela_2', function(Blueprint $table) {
$table->unsignedBigInteger('tenant_id')->nullable()->after('next_installment')->index();
$table->foreign('tenant_id')->references('id')->on('tenants');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('tabela_1', function(Blueprint $table) {
$table->dropColumn('tenant_id');
});
Schema::table('tabela_2', function(Blueprint $table) {
$table->dropColumn('tenant_id');
});
}
};
Da parte de preparar o banco é isso, vamos ao próximo passo, que é na aplicação
Classe JWT
Antes de iniciarmos com o código do multi-tenant, vamos dar uma olhada na minha classe do JWT.
<?php
namespace App\Tools\Auth;
use App\Enums\DateEnum;
use App\Models\User;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtTools
{
public static function createJWT(User $data): string
{
$payload = array(
'exp' => time() + DateEnum::TREE_HOUR_IN_SECONDS,
'iat' => time(),
'data' => $data
);
return JWT::encode($payload, env('SECRET_HASH'), 'HS256');
}
public static function validateJWT(string $authorization): bool|object
{
try {
$token = str_replace('Bearer ', '', $authorization);
$key = new Key(env('SECRET_HASH'), 'HS256');
return JWT::decode($token, $key);
} catch (\Exception $e) {
return false;
}
}
}
No createJWT, basicamente estou gerando o token com o model User, com isso, quando eu receber o token do frontend novamente e descriptografar, já tenho o model do User, sem a necessidade de ficar fazendo find no banco.
Já no validateJWT, vamos pegar esse token gerado anteriormente e descriptografar, podendo retornar o objeto JWT com o model User, ou false, caso não consiga descriptografar.
Classe TenantScope
Nessa classe, é onde vamos adicionar ao scope do Laravel a busca pelo parâmetro tenant_id de cada tabela, sem termos a necessidade de ficar fazendo em cada consulta ao banco.
A responsabilidade dela é adicionar a toda consulta a condição where filtrando pelo tenant_id no repository pesquisado, evitando assim esquecimentos.
<?php
namespace App\Scope;
use App\Enums\ConfigEnum;
use App\Tools\Auth\JwtTools;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$token = $_SERVER[ConfigEnum::X_USER_TOKEN] ?? '';
$user = JwtTools::validateJWT($token);
if (! $user) {
return;
}
$builder->where($model->getTable() . '.tenant_id', $user->data->tenant_id);
}
}
Aqui, basicamente pego o token da request, aquele feito pelo JWT mencionado anteriormente, caso consiga descriptografar ele, teremos o objeto do user, tendo assim o seu tenant_id. Caso não tiver, só dá um return, evitando quebras.
Calma, a obrigação do envio do token na request é responsabilidade de outra classe, vamos ver mais a frente.
Trait Tenantable
Essa é a trait que vai ser responsável por injetar o tenant_id no nosso model.
<?php
namespace App\Models\Trait;
use App\Scope\TenantScope;
use App\Models\Tenant;
use App\Tools\Auth\JwtTools;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
trait Tenantable
{
protected static function bootTenantable(): void
{
static::addGlobalScope(new TenantScope);
$token = $_SERVER['HTTP_X_MFP_USER_TOKEN'] ?? '';
$user = JwtTools::validateJWT($token);
if (! $user) {
return;
}
static::creating(function ($model) use ($user) {
$model->tenant_id = $user->data->tenant_id;
});
}
}
Basicamente a condicional é a mesma da classe anterior, a diferença é que que estamos definindo o tenant_id do model que usar essa trait.
Com isso, sempre que formos fazer um insert por exemplo, sempre teremos definido no model o tenant_id, sem a necessidade de popular manualmente esse atributo.
Com essa trait criada, vamos adicionar ela a todo model que deva fazer o uso do tenant_id. Basta adicionar o "use Tenantable;" em cada model.
Validando requests API e model Tenant
Na classe AuthServiceProvider, iremos fazer uma alteração no método boot. Aqui é onde iremos validar e obrigar o envio do token API e do token JWT.
public function boot(): void
{
Auth::viaRequest('authApi', function (Request $request) {
$userToken = $_SERVER[ConfigEnum::X_USER_TOKEN] ?? '';
$user = JwtTools::validateJWT($userToken);
$apiTokenEncrypted = bcrypt(env('SECRET_HASH'));
$apiToken = $request->header('API_TOKEN') ?? '';
return password_verify($apiToken, $apiTokenEncrypted) && $user ? new User() : null;
});
}
Caso esse método retorne o null, vai ser respondido o conteúdo definido dentro do método unauthenticated dentro da classe AuthenticateAPI. A minha eu deixei assim:
protected function unauthenticated($request, array $guards): ?string
{
$message = 'Tokens obrigatórios ausentes ou inválidos!';
abort(response()->json($message, Response::HTTP_UNAUTHORIZED));
}
Também temos que criar um model de Tenant. Não precisa de nada especial dentro dele, só ter ele criado mesmo.
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Tenant extends Model
{
use HasFactory;
}
Conclusão
Com isso tudo feito, já deve funcionar multi tenant na sua aplicação. Isso que eu mostrei aqui é algo relativamente super simples, porém senti que falta conteúdo falando sobre. Até achei um vídeo (aqui), porém não funcionou com o Vue devido a comunicação ser por API. Para funcionar na nessa modalidade tive que fazer as alterações mencionadas aqui.
No fim ficou algo mais ou menos assim:
Chegamos ao fim, até mais dev.
Top comments (0)