Gestione di un progetto multi-tenant in Laravel senza l’utilizzo di nessun package terze parti

Vi è mai capitato di dover gestire più applicazioni (legate al dominio o al sottodominio e/o sottocartella) all’interno dello stesso progetto?

Image for post
Image for post

Come si evince da questo schema, un’architettura multi-tenant risulta nel medio-lungo termine, più mantenibile rispetto alla classica architettura single-tenant, ma soprattutto permette di gestire un numero scalare di utenti ed applicazioni non facilmente gestibili singolarmente.

Oltre ad utilizzare una code-base comune, le applicazioni che fanno parte di un’architettura multi-tenant, a mio parere, dovrebbero anche condividere un solo database (anche se ci sono diverse scuole di pensiero a riguardo e alcuni miei colleghi preferiscono separare i database, scelta che a mio modesto parere, risulta diventare poco mantenibile e scalabile nel medio-lungo periodo).

I vantaggi per questo tipo di gestione delle applicazioni sono davvero molteplici ed ora vediamo come gestire il tutto in pochissimi passaggi:

Per prima cosa installiamo la versione corrente di Laravel:

composer create-project laravel/laravel esempio-multitenant

Successivamente compiliamo il file .env con i dati di connessione al nostro database MySql:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=esempio-multitenant
DB_USERNAME=root
DB_PASSWORD=root

E’ ora il momento di decidere in base a quale “discriminante” decideremo di gestire il multi-tenant. Ad esempio, possiamo decidere di utilizzare il dominio e quindi:
miosito.it > mostrerà il sito in italiano
miosito.com > mostrerà il sito in inglese
oppure
partendo dal sito mercatoannunci.it (che mostrerà tutti gli annunci)
mercatoauto.it > mostrerà solo gli annunci di auto
mercatomusica.it -> mostrerà solo gli annunci di strumenti musicali
e così via… ovviamente questi sono solo esempi e sta al vostro progetto e alla vostra fantasia il modo in cui utilizzare questo sistema che utilizzerà una sola code-base e un solo database comuni a tutti i tenant.
Possiamo anche decidere di creare una sorta di applicazione SaaS e quindi la discriminante sarà sul sottodominio, ad esempio:
utenteA.miodominio.com / utenteB.miodominio.com / utenteC.miodominio.com / ecc…
oppure possiamo impostare la “discriminante” su una sottocartella: dominio.com/promozioni/promoA / dominio.com/promozioni/promoZ

Per questo esempio prenderemo in considerazione l’idea di un software in SaaS (esempio comune per la gestione multi-tentant) e quindi apriremo il file /routes/web.php ed integreremo delle impostazioni:

<?php

//routes/web.php

use Illuminate\Http\Request;

$router->group(['domain' => '{subdomain}.{domain}.{tld}'], function () use ($router) {

$router->get('/', function () {
return view('welcome');
});

});

E’ ora il momento di creare una migration e un model per le nostre “Applications” che saranno richiamate e inizializzate dai “sottodomini”.
Per fare ciò lanceremo un comodo comando dalla CLI di Laravel:

php artisan make:model Application -m

Dopo aver lanciato questo comando troveremo un nuovo model Application.php nella cartella /app e un file YYYY_MM_DD_HHIISS_create_applications_table.php nella cartella /database/migration/ che compileremo in questo modo:

<?php

//database/migration/YY_MM_DD_HHIISS_create_applications_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateApplicationsTable extends Migration
{

public function up()
{
Schema::create('applications', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('subdomain')->uniqid()->index();
$table->boolean('active')->default(0);
$table->string('name')->nullable();
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('applications');
}
}

Eseguiamo ora le migration tramite il comando che creerà le tabelle nel nostro database:

php artisan migrate

A questo punto inseriamo dei dati di prova all’interno del DB… questo potremmo farlo anche con dei comodi Seeders se volessimo, in ogni caso dobbiamo ottenere una tabella dati simile a questa:

Image for post
Image for post

Come potete notare i primi 2 clienti sono “attivi” e quindi mi aspetto che la loro application venga mostrata dal mio progetto Laravel mentre il cliente C, se proverà a digitare clientec.miosito, dovrebbe trovarsi davanti a un errore 404, come se la sua applicazione non esistesse.
Per fare questo dobbiamo creare un Middleware che, intercettando la richiesta dell’utente, verifica se l’application è attiva o meno e, se attiva, passa i dati riguardanti l’application stessa all’oggetto $request.

Per creare un middleware lanciamo il comando:

php artisan make:middleware MultiTenant

Questo comando genererà un file MultiTenant.php all’interno della cartella /app/Http/Middleware/ che dovremo registrare tramite l’alias multi.tenant nel file /app/Http/kernel.php nell’oggetto $routeMiddleware:

protected $routeMiddleware = [
//other Laravel middlewares
'multi.tenant' => \App\Http\Middleware\MultiTenant::class,
];

Una volta registrato il middleware aggiorneremo il nostro file route:

<?php

//routes/web.php

use Illuminate\Http\Request;

$router->group(['domain' => '{subdomain}.{domain}.{tld}'], function () use ($router) {
$router->group(['middleware' => ['multi.tenant']], function () use ($router) {

$router->get('/', function (Request $request) {
$data = ['name' => $request->application->name];
return view('welcome')->with($data);
});

});
});

A questo punto dovremo creare, all’interno del middleware, una logica di verifica delle request basata sul sottodominio, apriamo quindi il file /app/Http/Middleware/MultiTenant.php e modifichiamolo in questo modo:

<?php

//app/Http/Middleware/MultiTenant.php

namespace App\Http\Middleware;

use Closure;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Application;

class MultiTenant
{
public function handle(Request $request, Closure $next)
{

//Get application slug
$subdomain = $request->subdomain;
if(!$subdomain) {
abort(404);
}

//Get Application info from DB
$application = Application::where('subdomain', $subdomain)->where('active', 1)->first();
if(!$application) {
abort(404);
}

//Set custom vars in $request
$request->application = $application;

//End of middleware
return $next($request);

}
}

Ora modifichiamo la vista /resources/views/welcome.blade.php

<!-- /resources/views/welcome.blade.php -->

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Laravel MultiTenant</title>

<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">

<!-- Styles -->
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: 'Nunito', sans-serif;
font-weight: 200;
height: 100vh;
margin: 0;
}

.full-height {
height: 100vh;
}

.flex-center {
align-items: center;
display: flex;
justify-content: center;
}

.position-ref {
position: relative;
}

.top-right {
position: absolute;
right: 10px;
top: 18px;
}

.content {
text-align: center;
}

.title {
font-size: 84px;
}

.links > a {
color: #636b6f;
padding: 0 25px;
font-size: 13px;
font-weight: 600;
letter-spacing: .1rem;
text-decoration: none;
text-transform: uppercase;
}

.m-b-md {
margin-bottom: 30px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="content">
<div class="title m-b-md">
{{ $name }}
</div>
</div>
</div>
</body>
</html>

Per finire, creiamo nel nostro file hosts locale 3 domini “fake” che puntino al nostro singolo progetto Laravel:

127.0.0.1 clientea.miosito.local
127.0.0.1 clienteb.miosito.local
127.0.0.1 clientec.miosito.local

Avviamo il webserver locale e testiamo…

Se richiamiamo i sottodomini “clientea” e “clienteb” otterremo una welcome page, mentre se digitiamo “clientec” dovrebbe comparire un errore 404 generato dal nostro Middleware custom.

In conclusione, esistono molti modi per creare un’architettura multi-tenant… Questo appena mostrato è un esempio “base” e a “portata di tutti” che vuole semplicemente essere “uno spunto” per chi vuole avvicinarsi al mondo di Laravel.

Puoi scaricare lo script appena creato da qui:
https://github.com/andreapollastri/laravel-multitenant-medium-article

Written by

I’m a software engineer based in Milan. Always looking to discover new development methods and technologies, I am an open source enthusiast and supporter.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store