I have implemented the simplest example using the Spatie docs for multitenancy, that is working perfectly fine. Now, I intend to use multiple second-level domains for each tenant I have.
For example; I have 2 tenants company-a
and company-b
and they are being served at company-a.localhost
and company-b.localhost
, now what I want is that when I visit company-a.admin.localhost
, it should tell me COMPANY-A ADMIN and If I visit company-a.employee.localhost
, it should tell me COMPANY-A EMPLOYEE.
I have tried using subdomain
on routes in RouteServiceProvider
like the following:
Route::middleware('web')
->group(base_path('routes/security.php'));
Route::domain($this->baseDomain('admin'))
->middleware('web')
->name('admin.')
->group(base_path('routes/admin.php'));
Route::domain($this->baseDomain('employee'))
->middleware('web')
->name('employee.')
->group(base_path('routes/employee.php'));
private function baseDomain(string $subdomain = ''): string
{
if (strlen($subdomain) > 0) {
$subdomain = "{$subdomain}.";
}
return $subdomain . config('app.base_domain');
}
Without subdomain, it works fine, but the routes with second-level domain, it falls to base level domain route and does not get the current tenant. What am I missing here? Is this even possible to implement.
Thankyou.
CodePudding user response:
Take, for example, the route:
Route::domain('{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
});
The binding fields would be ['subdomain', 'param1', 'param2']
, and the compiled route would have it's regexes declared as
regex => "{^/foo/(?P<param1>[^/] )/(?P<param2>[^/] )$}sDu",
hostRegex => "{^(?P<subdomain>[^\.] )\.example\.com$}sDiu"
Where ^(?P<subdomain>[^\.] )\.
will explicitly stop capturing when finding a dot, in this case the delimiter between groups.
However, these regexes are overridable by using the where
method. You could declare the above route as
Route::domain('{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
})->where('subdomain', '(.*)');
In the compiled route , the hostRegex would be now
hostRegex => "{^(?P<subdomain>(?:.*))\.example\.com$}sDiu"
Meaning it will capture anything preceding .example.com
. If you requested company-a.admin.example.com
, $subdomain
would be company-a.admin
.
You could also declare a route with two binding fields in its domain:
Route::domain('{subsubdomain}.{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
});
Which might be more useful if you wanted subsubdomains to imply a hierarchy.
CodePudding user response:
I have achieved this by using some checks, in RouteServiceProvider
, I have not used the actual domain
function on Route
like we do normally i.e. Route::domain('foo.bar')
. The reason was that, the Spatie
package use a kind of middleware Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class
which runs whenever we hit the domain with tenant comapny-a.localhost
. And it gets the tenant from hostname i.e comapny-a.localhost
.
public function findForRequest(Request $request):?Tenant
{
$host = $request->getHost();
return $this->getTenantModel()::whereDomain($host)->first();
}
In my RouteServiceProvide
:
$this->routes(function () {
$class = 'security';
$middleware = 'web';
if (Str::contains(request()->getHost(), 'admin')) {
$class = 'admin';
} elseif (Str::contains(request()->getHost(), 'employee')) {
$class = 'employee';
} elseif (Str::contains(request()->getHost(), 'api')) {
$class = 'api';
$middleware = 'api';
}
Route::middleware($middleware)
->name("$class.")
->group(base_path("routes/${class}.php"));
});
As In my scenario, I had only these 2 kind of second-level domains and so, I just checked if this particular keyword exists in the hostname and choosing the file and middleware accordingly.
I also overrided the DomainTenantFinder
class and in multitenancy.php
config file:
public function findForRequest(Request $request): ?Tenant
{
$host = $request->getHost();
$host = str_replace('admin.', '', $host);
$host = str_replace('employee.', '', $host);
$host = str_replace('api.', '', $host);
$tenant = $this->getTenantModel()::whereDomain($host)->first();
if (empty($tenant)) {
abort(404);
}
return $tenant;
}
I have acheived the desired outcome, however, I have a security concern, specially in RouteServiceProvider
logic. Thought??