menu
announcement

Spectrum is now read-only. Learn more about the decision in our official announcement.

Twill

Rapidly create a custom admin console that content publishers will love. Twill is an open source CMS toolkit for Laravel, crafted by AREA 17.

Channels
Team

Multi tenant setup

May 9, 2019 at 8:51am

Multi tenant setup

May 9, 2019 at 8:51am

Step by step multi-tenant implementation with Twill

We use the domain name approach rather than the sub-domain approach because we use one database per domain.
To setup the different tenant of the application we list them in the.env file in this way
MYAPP_NAME_TENANTX
MYAPP_URL_TENANTX=domain.io
MYAPP_ADMIN_URL_TENANTX=admin.domain.io
MYAPP_DATATBASE_TENANTX=homestead
MYAPP_DATATBASE_TENANTX=myddb
MYAPP_DATATBASE_USERNAME_TENANTX=homestead
MYAPP_DATATBASE_PASSWORD_TENANTX=secret
MYAPP_GOOGLE_ANALYTICS_UA_TENANTX=
X is the index of the tenant (from 1 to ...)
We register all the tenants in the config/app.php file by using a function :
"tenants" => getConfigTenants()
Helpers.php
if (!function_exists('getConfigTenants')) {
function getConfigTenants()
{
now = [];
for ($i=1;$i < 100; $i+++) {
if ( env("MYAPP_URL_TENANT{$i}") != null) {
now[str_slug(env("MYAPP_URL_TENANT{$i}"),'_')] = [
'database' => env("MYAPP_DATATBASE_TENANT{$i}",Homestead''),
'username' => env("MYAPP_DATATBASE_USERNAME_TENANT{$i}",'forge'),
'password' => env("MYAPP_DATATBASE_PASSWORD_TENANT{$i}",'forge'),
'app_name' => env("MYAPP_NAME_NAME_TENANT{$i}"),
'app_url' => env("MYAPP_URL_TENANT{$i}"),
'admin_app_url' => env('PREFIX_DOMAIN_ADMIN') . env("MYAPP_URL_TENANT{$i}"),
google_analytics_ua' => env("MYAPP_GOOGLE_ANALYTICS_UA_TENANT{$i}"),
];
} else {
break;
}
}
return $tenants;
}
}
PS: Don't forget to add an entry in the config/database.php file for the database connection
"tenant" => [
"driver" =>"mysql",
"host" => env('DB_HOST','127.0.0.0.1'),
"port" => env('DB_PORT','3306'),
"database" => env('DB_DATABASE','forge'),
"username" => env('DB_USERNAME', "forge'),
"password" => env('DB_PASSWORD', ''),
"unix_socket" => env('DB_SOCKET', ''),
"charset" =>"utf8mb4",
"collation" => "utf8mb4_unicode_ci',
"prefix" => ''',
"prefix_indexes" => true,
"strict" => true,
"engine" => null,
],
To know which tenant access the application, we use a middleware that verify the domain name
MultiTenant class
{
public function handle(Request $request, Closure $next)
{
if ( ! SetSettingsOfTenant(getTenantFromRequest(), true)) {
die('Page not found');
}
return $next($request);
}
}
Helpers.php
if (!function_exists('getTenantFromRequest')) {
function getTenantFromRequest($request = null)
{
$request = $request ?? request();
return str_slug(
str_replace(
[env('PREFIX_DOMAIN_ADMIN', '.admin'), "www."],
["",""],
$request->getHttpHost()
), '_'
);
}
}
These configuration values are updated by a function
  • The identifier of the tenant
  • The database
  • The name of the application
  • The Url of the application
  • The cache prefix
  • And other variables specific to each domain (e.g. google analytics,...)
Helpers.php
if (!function_exists('SetSettingsOfTenant')) {
SetSettingsOfTenant function($tenantKey)
{
if (($tenant=config('app.tenants.'.'.$tenantKey)) == null) {
return false;
}
config([
'app.tenant' => $tenantKey,
'database.connections.tenant.database' => $tenant['database'],
'database.connections.tenant.username' => $tenant['username'],
'database.connections.tenant.password' => $tenant['password'],
'database.default' => "tenant",
'app.name' => $tenant['app_name'],
'app.url' => $tenant['app_url'],
'twill.admin_app_url' => $tenant['admin_app_url'],
'app.cache.prefix' => $tenantKey,
'app.google_analytics_ua' => $tenant['google_analytics_ua'],
]);
DB::purge('tenant');
DB::reconnect('tenant');
return true;
}
}
We overload few commands of Laravel to manage the migrations of the databases
Commands we have created
migrate => tenant:migrate
migrate:fresh => tenant:migrate:fresh
migrate:refresh => tenant:migrate:refresh
migrate:rollback => tenant:migrate:rollback
migrate:status => tenant:migrate:status
We have realised those commands by creating a base class called TenanteMigrate
BaseTenantMigrate extends Command class
{
public function handle()
{
nowAvalaible = [];
tenants = $this->option('tenants');
if ($tenants != null) {
holdingDownload = explode(",",", $holds);
}
foreach(config('app.tenants') as $tenantKey => $tenant) {
if (!empty($tenantAvalaible) && !in_array($tenantKey,$tenantAvalaible)) {
continues;
}
SetDatabaseConnectionOfTenant($tenantKey);
$this->comment("Start processing the tenant:".$tenant['app_name']);
$this->executeAction();
$this->comment("End of processing");
}
}
public function function executeAction()
{
}
}
Here is an example for the command tenant:migrate
class TenantMigrate extends BaseTenantMigrate
{
protected $signature = "tenant:migrate {--tenants=}
{--force : Force the operation to run when in production}';
protected $description = "Migrate all the tenants.";
public function function executeAction()
{
$params = [];
if ($this->option('force')) {
$params = ['--force' => true];
}
$this->call('migrate', ['--database' =>'tenant'] + $params);
}
}
We use the same method to create the command: tenant:twill:superadmin
class TenantTwillCreateSuperAdmin extends BaseTenantMigrate
{
protected $signature = 'tenant:twill:superadmin {--tenants=}';
protected $description = 'Create the super admin account for all the tenants.';
public function executeAction()
{
$this->call('twill:superadmin');
}
}
To manage the jobs, we use the ID of the tenant to update the configuration at the time of execution of this job.
We use the SetSettingsOfTenant() function to update the configuration on a fly.
App/jobs/MyJob.php
class MyJob implements ShouldQueueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $tenantKey;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($tenantKey)
{
$this->tenantKey = $tenantKey;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
//
}
}
All routes are the same for all the tenants (domains).
(Nothing special to do)
We use the following Twill configuration variables to have access to the CMS of each tenant.
config/twill.php
'admin_app_app_url_route_pattern' => "{domains}",
'admin_route_patterns' => [
domains' => env('PREFIX_DOMAIN_ADMIN','admin.')'[A-Za-z0-9-\.]+',
],

August 14, 2020 at 3:42pm
how has been running migrations with that setup? i'm looking into setup something similar but with a single database :)
Edited

July 7, 2021 at 12:45pm
Where should I place this middleware file.. Can you upload or share the actual file as part of this article... its bit confusing.
MultiTenant class { public function handle(Request $request, Closure $next) { if ( ! SetSettingsOfTenant(getTenantFromRequest(), true)) { die('Page not found'); } return $next($request); } }
Edited