Еще один способ реализации пользовательских доменов и субдоменов в Ларавел для мультитенантных приложений. Multitenancy — множественная аренда, когда один экземпляр приложения, обслуживает множество тенантов («арендаторов»).
Начальный способ
Первый способ, которым я реализовал пользовательские домены в Laravel, это добавил глобальный паттерн маршрута в RouteServiceProvider, который изменил регулярное выражение для сопоставления субдоменов, чтобы учесть полные домены:
Route::pattern('domain', '[a-z0-9.\]+');
Это позволило использовать функцию доменной маршрутизации следующим образом:
Route::domain('{domain}')->group(function() {
Route::get('users/{user}', function (Request $request, string $domain, User $user) {
// здесь ваш код
});
});
И хотя это замечательно работает, но теперь необходимо ко всем маршрутам добавлять аргумент $domain, даже если для данного маршрута это и не нужно. Тогда мне показалось это приемлемым решением, но я продолжал искать наилучший способ.
Наилучший способ
В новом решении требовалось полностью забыть, что в Laravel есть маршрутизация субдоменов. Вместо этого мы будем использовать мидлвар для поиска подходящего тенанта на основе запроса хоста и добавления данных в объект запроса, чтобы потом их можно было использовать далее в приложении. Самой простой реализацией этого может быть следующий мидлвар-класс:
namespace App\Http\Middleware;
use Closure;
use App\Tenant;
class CustomDomain
{
/**
* Обрабатываем входящий запрос
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$domain = $request->getHost();
$tenant = Tenant::where('domain', $domain)->firstOrFail();
// Добавляем домен и тенанта в объект запроса
// для дальнейшего использования в приложении
$request->merge([
'domain' => $domain,
'tenant' => $tenant
]);
return $next($request);
}
}
Здесь я предполагаю, что у вас есть модель Tenant с атрибутом domain, который содержит полное доменное имя, которое вы хотите использовать для данного тенанта.
Теперь в файл app/Http/Kernel.php вы можете добавить мидлварную группу или маршрутный мидлвар, который будет содержать этот новый мидлвар. Мне больше нравится вариант с созданием новой группы для доменной маршрутизации. В дальнейшем она пригодится, если появятся новый мидлвары для кастомной доменной маршрутизации.
protected $middlewareGroups = [
...
'domain' => [
\App\Http\Middleware\CustomDomain::class,
],
...
];
Теперь я могу назначить эту мидлварную группу любым маршрутам, которые должны обслуживаться в кастомном домене. Обычно я создаю новый файл routes/tenant.php, а затем, в файл app/Providers/RouteServiceProvider.php, добавляю для него новый метод вместе с пространством имён и префиксом:
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
$this->mapTenantRoutes();
}
protected function mapTenantRoutes()
{
Route::middleware(['web', 'auth', 'domain'])
->namespace("$this->namespace\Tenant")
->name('tenant.')
->group(base_path('routes/tenant.php'));
}
Последняя часть головоломки — как получить доступ к информации о тенанте из контроллеров, шаблонов и других частей вашего приложения. В отличие от предыдущего решения, здесь нет необходимости добавлять аргумент $domain к маршрутам. Вместо этого вы можете прочитать информацию о тенанте прямо из объекта запроса. Вот пример контроллера, который делает это:
public function index(Request $request)
{
$tenant = $request->tenant;
...
}
Если вы используете blade-шаблоны, то мидлвар CustomDomain является отличным местом, откуда можно расшарить нужные данные, которые понадобятся в шаблонах, зависящих от тенантов.
namespace App\Http\Middleware;
use Closure;
use App\Tenant;
use Illuminate\Support\Facades\View;
class CustomDomain
{
/**
* Обрабатываем входящий запрос
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$domain = $request->getHost();
$tenant = Tenant::where('domain', $domain)->firstOrFail();
// Добавляем домен и тенанта в объект запроса
// для дальнейшего использования в приложении
$request->merge([
'domain' => $domain,
'tenant' => $tenant
]);
// Расшариваем данные тенанта
// для удобной настройки шаблонов
View::share('tenantColor', $tenant->color);
View::share('tenantName', $tenant->name);
return $next($request);
}
}
Прочие факторы
Расскажу еще о некоторых вещах, с которыми вы можете столкнуться при использовании этого способа реализации пользовательских доменов.
Аутентификация
Скорее всего, вы захотите, чтобы пользователи вашего приложения могли входить в систему только под тем тенантом, которому они принадлежат. Для этого вы можете сохранить атрибут tenant_id в модели User и изменить свою аутентификацию, чтобы учесть это при входе в систему. Вы можете сделать это, переопределив метод credentials в файле app/Http/Controllers/Auth/LoginController.php. Дефолтный метод (который находится в трейте AuthenticatesUsers ) выглядит так:
protected function credentials(Request $request)
{
return $request->only($this->username(), 'password');
}
Узнать тенанта так же просто, как изменить его содержимое:
protected function credentials(Request $request)
{
$credentials = $request->only($this->username(), 'password');
$credentials['tenant_id'] = optional($request->tenant)->id;
return $credentials;
}
Теперь пользователи смогут входить только под доменом тенанта, с которым связана их пользовательская модель.
Возможно вам будет интересно, как происходит сопоставление маршрутов аутентификации. По умолчанию эти маршруты добавляются в routes/web.php, но, если вы хотите использовать в них тенантный мидлвар (для настройки или по каким-то другим причинам), то вам лучше создать новый метод в RouteServiceProvider, например так:
protected function mapAuthRoutes()
{
Route::middleware(['web', 'domain'])
->namespace("$this->namespace\Auth")
->name('auth.')
->group(base_path('routes/auth.php'));
}
Субдомены
Хотя целью всего этого является включение поддержки пользовательских доменов в Laravel, но вы, конечно же, можете обслуживать и субдомены, используя тот же подход, что описан выше. Основное различие между этим решением и встроенной функцией маршрутизации субдоменов заключается в том, что при таком подходе вам нужно будет хранить полный домен для своего тенанта, а не только префикс субдомена.
Корневые и безтенантные домены
В некоторых случаях, вам будет необходимо обслужить что-либо в корневом домене вашего приложения или в домене, у которого нет тенанта. Например панель администратора или какой-либо другой функционал. Один из способов, который здесь можно использовать, это добавить в конфиг настройку нужного домена и добавить проверку на существование тенанта в мидлвар CustomDomain.
namespace App\Http\Middleware;
use Closure;
use App\Tenant;
use Illuminate\Support\Facades\View;
class CustomDomain
{
/**
* Обрабатываем входящий запрос
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$domain = $request->getHost();
$tenant = Tenant::where('domain', $domain)->first();
if (!$tenant) {
$adminDomain = config('app.admin_domain');
if ($domain != $adminDomain) {
abort(404);
}
}
// Добавляем домен и тенанта в объект запроса
// для дальнейшего использования в приложении
$request->merge([
'domain' => $domain,
'tenant' => $tenant
]);
if ($tenant) {
View::share('tenantColor', $tenant->color);
View::share('tenantName', $tenant->name);
}
return $next($request);
}
}
Для данных шаблона, можно добавить резервный вариант непосредственно прямо в мидлвар или, например, в метод boot файла app/Providers/AppServiceProvider.php
public function boot()
{
...
View::share('tenantColor', 'gray');
View::share('tenantName', 'Admin');
...
}
Резюме
На этом всё! В целом, я считаю, что этот способ является наболее чистым методом реализации пользовательских доменов (и даже субдоменов) в Laravel.
Автор: Joe Lennon
Перевод: Алексей Широков
Наш Телеграм-канал — следите за новостями о Laravel.
