Multi tenant setup
May 9, 2019 at 8:51amMulti tenant setup
May 9, 2019 at 8:51amStep 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_TENANTXMYAPP_URL_TENANTX=domain.ioMYAPP_ADMIN_URL_TENANTX=admin.domain.ioMYAPP_DATATBASE_TENANTX=homesteadMYAPP_DATATBASE_TENANTX=myddbMYAPP_DATATBASE_USERNAME_TENANTX=homesteadMYAPP_DATATBASE_PASSWORD_TENANTX=secretMYAPP_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 createdmigrate => tenant:migratemigrate:fresh => tenant:migrate:freshmigrate:refresh => tenant:migrate:refreshmigrate:rollback => tenant:migrate:rollbackmigrate: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
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);
}
}