Back-End

4 jun, 2019

Simples Configuração Multi Tenant no Laravel

Publicidade

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.

Simples Configuração Multi Tenant no Laravel
Simples Configuração Multi Tenant no Laravel

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 SelectTenantControllerpara 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.