Ad

Laravel 6 - Setting Up A Multi-tenant Application With Nested Tenants

I am currently trying to figure out the best way to set up multi-tenancy for my system. The issue I'm facing is that a tenant doesn't always have to be a sub-domain but can be set up as part of a sub-domain, where the sub-domain can have multiple tenants. I can't seem to find anything online that would help me set this up in Laravel 6.

System Requirements:

  • A server can have many sub-domains
  • A sub-domain can be a tenant
  • A sub-domain can have many tenants
  • A tenant can have many users
  • a tenant can have different features

The system has to be set up with a single database that will use tenant_id to determine which data belongs to a tenant.

I am currently storing all sub-domain data in a table "subdomains" with the following structure:

id 
subdomain (unique)
status 
nested_tenants (yes/no)

where the column nested_tenants determines whether or not the sub-domain is a tenant(=0) itself or has multiple tenants(=1). if the sub-domain does not have nested tenants then we set tenant_id=subdomain

if the sub-domain does have nested tenants then we store all of these in a table with structure:

id
subdomain (the sub-domain it belongs to)
tenant (the tenant - unique field)
name
status

and we set tenant_id=tenant from this table.

if we have nested tenants for a sub-domain then we cannot determine what the current tenant is until the user logs in. we would have to get the tenant_id from the user details.

My current set up:

I've been following this article and have set up the following:

I have two models Subdomain, Tenant

Routes/web.php:

Route::group([
    'middleware' => \App\Http\Middleware\IdentifySubdomain::class,
    'as' => 'tenant:',
    'namespace' => 'Tenant'
], function () {
    // custom auth routes
    Route::get('/login', 'Auth\[email protected]')->name('login');

    Route::post('/login', 'Auth\[email protected]');

    Route::get('/home', '[email protected]')->name('home');
});

Middleware IdentifySubdomain:

class IdentifySubdomain
{

    protected $tenantManager;

    public function __construct(TenantManager $tenantManager) {
        $this->tenantManager = $tenantManager;
    }

    public function handle($request, Closure $next)
    {
        /** need to check whether subdomain is valid 
        * if subdomain is valid return the request page else error message. 
        * if subdomain is true it will check the nested_tenants value from db. 
        * if nested_tenants is false it will set the tenant to current subdomain 
        * else the tenant is not set yet. 
        */ 
        // get host domain and subdomain domain
        $host = $request->getHost();

        // get subdomain position
        $pos = strpos($host, env('TENANT_DOMAIN'));
        $subdomain = substr($host, 0, $pos - 1);

        if ($pos !== false && $this->tenantManager->checkSubdomain($subdomain)) {
            return $next($request);
        }

        throw new NotFoundHttpException;
    }
}

TenantManager:

class TenantManager {

    private $tenant;

    public function setTenant(?Tenant $tenant) {
        $this->tenant = $tenant;
        return $this;
    }

    public function getTenant(): ?Tenant {
        return $this->tenant;
    }

    public function loadTenant(string $identifier): bool {

        $tenant = Tenant::query()->where('tenant', '=', $identifier)->first();
        if ($tenant) {
            $this->setTenant($tenant);
            return true;
        }

        return false;
    }

    public function checkSubdomain(string $identifier) : bool {
        $subdomain = Subdomain::query()->where('subdomain', '=', $identifier)->first();

        if ($subdomain) {
            if ($subdomain->nested_tenants) {
                // tenant not found yet so do not set tenant
                return true;
            } else {
                return $this->loadTenant($identifier);

            }            
        } 

        return false;
    }
}

Service Provider

class TenantServiceProvider extends ServiceProvider
{
    public function register()
    {        
        $manager = new TenantManager;
        $this->app->instance(TenantManager::class, $manager);
        $this->app->bind(Tenant::class, function() use ($manager) {   
            $tenant = $manager->getTenant();
            if ($tenant === null) {
                return new Tenant;
            }        
            return $manager->getTenant();
        });

    }
}

Login Controller:

class LoginController extends Controller
{    
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }
    ...
    public function login(Request $request, Tenant $tenant) {
        $request->validate([
            'email' => ['required', 'email', 'max:255'],
            'password' => ['required'],
        ]);
        $credentials = $request->only('email', 'password');
        $credentials['status'] = 1;
        if ($tenant->id) {    
            $credentials['tenant_id'] = $tenant->tenant;            
        } 
        if (Auth::attempt($credentials)) {                           
            return redirect()->intended('home');           
        } 

        return Redirect::to('login')->withSuccess('Login Failed! You entered invalid credentials');
    }
    ...
}

Issues

My main concern is that I don't feel like this is the best approach to keeping track of the tenant. I need it so that once the tenant is set I can use it throughout the application, without always first checking if the user is authenticated first - to then get the tenant. I am currently adding Tenant $tenant to the controller methods where I need tenant related data, but is a there better way of going about this?

Any advice on how I could improve my current set up would be helpful.

Ad

Answer

I think you should implement Traits to add tenant constraints for example: in models:

BelongsToTenantModelTrait{

public static function bootBelongsToTenantModelTrait(){
       static::addGlobalScope(function ($model){
            model->where('tenant_id',auth()->user()->tenant->id);
            //Or any similar logic
        });
} 

and other traits to controllers if needed.
You may also add middlewares like AuthTenant if needed as well.
I think this way should decouple the tenant-related logic as much as possible.
Let me know what you think.

Ad
source: stackoverflow.com
Ad