Еще один способ реализации пользовательских доменов и субдоменов в Ларавел для мультитенантных приложений. 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.