Artigo original: Simples Configuração Multi Tenant no Laravel
O que é Multi Tenant?
É uma arquitetura na qual uma única instância de uma aplicação atende a vários clientes. Cada cliente é chamado de Tenant (inquilino). Os inquilinos podem ter a capacidade de personalizar algumas partes do aplicativo, como a cor da interface do usuário ou das regras de negócios, mas não podem alterar o código.
O Laravel torna muito fácil fazer isso. Tudo o que você precisa é de uma configuração de conexão, um Middleware, uma Trait e configurar suas Models. Vamos fazer isso na prática.
Configurações de Conexão
No seu arquivo config/database.php vamos definir 2 conexões. Note que eu apago a conexão mysql, por isso é necessário configurar o arquivo .env para o novo driver de conexão.
'connections' => [
'main' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'bando_de_dados'),
'username' => env('DB_USERNAME', 'usuario'),
'password' => env('DB_PASSWORD', 'senha'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
'tenant' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => '',
'username' => '',
'password' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
]
]
O Middleware
Sempre garanta que a conexão exista. Garanta que todas as rotas que devem se conectar ao banco de dados de algum tenant (inquilino) usem esse middleware. Na minha situação particular, o usuário selecionaria um cliente (locatário) de uma lista e manipularia os dados desse cliente, daí o uso da sessão. Mas eu poderia facilmente ter 2 middlewares (WebTenant, ApiTenant) e confiar em tokens para escolher uma conexão de inquilino também.
<?php
namespace App\Http\Middleware;
use App\Models\Empresa;
use App\Support\Controller\TenantConnector;
use Closure;
class Tenant {
use TenantConnector;
/**
* @var Empresa
*/
protected $Empresa;
/**
* Tenant constructor.
* @param Empresa $empresa
*/
public function __construct(empresa $empresa) {
$this->empresa = $empresa;
}
/**
* Trata a requisição
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next) {
if (($request->session()->get('tenant')) === null)
return redirect()->route('home')->withErrors(['error' => __('Você não selecionou nenhum cliente.')]);
// Busca a empresa pelo id armazenado na sessão
$empresa = $this->empresa->find($request->session()->get('tenant'));
// Conecta no banco escolhido e colocar a variavel $empresa na sessão
$this->reconnect($empresa);
$request->session()->put('empresa', $empresa);
return $next($request);
}
}
TenantConnector (A Trait)
Não há muito o que falar aqui, basta ter sua conexão tenant definida.
<?php
namespace App\Support;
use App\Models\Empresa;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
trait TenantConnector {
/**
* Altera a conexão tenant para a empresa selecionada
* @param Empresa $Empresa
* @return void
* @throws
*/
public function reconnect(Empresa $empresa) {
// Apaga a conexão tenant, forçando o Laravel a voltar suas configurações de conexão para o padrão.
DB::purge('tenant');
// Setando os dados da nova conexão.
Config::set('database.connections.tenant.host', $empresa->mysql_host);
Config::set('database.connections.tenant.database', $empresa->mysql_database);
Config::set('database.connections.tenant.username', $empresa->mysql_username);
Config::set('database.connections.tenant.password', $empresa->mysql_password);
// Conecta no banco
DB::reconnect('tenant');
// Testa a nova conexão
Schema::connection('tenant')->getConnection()->reconnect();
}
}
As Models
Uma model principal terá a conexão main e nada mais.
<?php
namespace App\Models\;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Admin extends Authenticatable {
use Notifiable;
protected $connection = 'main';
}
A model Empresa (cliente/inquilino) foi diferente. Eu usei a Trait TenantConnector e escrevi o método connect(). Isso me permite fazer coisas como Empresa:: find($id)->connect();
<?php
namespace App\Models;
use App\Support\TenantConnector;
use Illuminate\Database\Eloquent\Model;
/**
* @property string mysql_host
* @property string mysql_database
* @property string mysql_username
* @property string mysql_password
*/
class Empresa extends Model {
use TenantConnector;
protected $connection = 'main';
/**
* @return $this
*/
public function connect() {
$this->reconnect($this);
return $this;
}
}
Uma Model de Tenant usará somente a conexão tenant.
<?php
namespace App\Models\Tenant;
use Illuminate\Database\Eloquent\Model;
class EmailQueue extends Model {
protected $connection = 'tenant';
}
A última coisa seria o SelectTenantController, para permitir que você defina a sessão que o middleware espera.
/**
* @GET
* @param Request $request
* @param $empresa
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function select(Request $request, $empresa) {
$this->reconnect($this->empresa->findOrFail($empresa));
$request->session()->put('tenant', $empresa);
return redirect('/');
}
Conclusão
O Laravel torna mais fácil ter duas configurações de conexão. Rotas que se conectarão a um banco de dados específico podem facilmente ter um middleware para garantir que a conexão exista. Você pode facilmente escolher a conexão para cada Model (ou ter uma MainModel/TenantModel e estendê-los). Tudo está configurado e você tem uma aplicação Laravel capaz de se conectar a vários bancos de dados.