Search code examples
phplaravel

Multitennancy by domain or subdomain


I'm trying to implement some basic multitenancy where each user can have their own site using a subdomain of my domain (e.g. tenant.mydomain.com), or use their own custom domain with any subdomain (e.g. www.theirdomain.com or store.theirdomain.com). On sign-up, they get a subdomain, but could later change to a custom domain. The system needs to find the correct tenant regardless of whether they access the route using the subdomain or domain, and ideally I'd like to use the same routes, controller functions, etc. for both subdomain and custom domain access.

I have the subdomain of my own domain functioning properly with the following:

Route::group(['domain' => '{subdomain}' . env('SESSION_DOMAIN')], function () {
  // Tenant-specific routes here
});

Each route receives the subdomain and I can look up their tenant accordingly in the corresponding controller method.

The difficulty I'm having is using a custom domain. I can do something like the following...

Route::group(['domain' => '{prefix}.{domain}.{suffix}'], function() {
  Route::get('/', function($prefix, $domain, $suffix) {
    return "Domain is: $prefix.$domain.$suffix";
  });
});

... but managing three variables for all routes is a bit unwieldy, and I can't use the same controller functions or blade files as with a subdomain as there is a different number of parameters required. I also have the option to not use an array group and allow any domain/subdomain to access the routes and query the domain / subdomain as follows...

$current_url = Request::url();
$current_url = str_replace(['https://', 'http://'], '', $current_url);
$subdomain = array_shift(explode('.', $current_url));
// Now look up the tenant based on the domain if is a custom domain or subdomain if it is our domain

But, I'm not really sure where that code, i.e. in the controller methods, or somewhere that's more global (where would that be?). It would also make referencing routes in blade files fairly convoluted.

Does anyone have any suggestions?


Solution

  • After some preliminary testing, here is the solution I've come up with...

    • Create a new middleware class - in the handle function, get the URL with $request->url() and parse it to see if it's a custom domain or a subdomain of my domain. Query the tennant ID based on the subdomain or domain and add it to a config variable using config(['tennant.id' => $tennant->id]);
    • Create a route group with the middleaware Route::middleware(['getTennant'])->group(function () { ... });
    • This config variable can be accessed from anywhere, and it seems like it's not cached in a way that causes unanticipated behaviour from my initial testing.

    The middleware code I used is...

    Here is my middleware code:

    <?php
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    use App\Models\Tennant;
    
    class getTennant
    {
        /**
         * Handle an incoming request.
         *
         * @param  \Illuminate\Http\Request  $request
         * @param  \Closure  $next
         * @return mixed
         */
        public function handle(Request $request, Closure $next)
        {
            $current_url = $request->url();
            $current_url = str_replace(['https://', 'http://'], '', $current_url);
            if (str_contains($current_url, env('SESSION_DOMAIN'))) {
                // This is a subdomain
                $current_url = explode('.', $current_url);
                $subdomain = array_shift($current_url);
                // Look up the appropriate tennant by subdomain here
                $tenant = Tenant::where('subdomain', $subdomain)->first();
            }
            else {
                // This is a full custom domain - look up the tennant based on domain
                $tenant = Tennant::where('domain', $current_url)->first();
            }
            if ($tenant) {
                config(['tenant.id' => $tenant->id]);
            }
            else {
                abort(404);
            }
            return $next($request);
        }
    }