Поддомены для пользователей в Laravel

Источник: «Subdomains For Users in Laravel»
В статье рассмотрим, как регистрировать поддомены для пользователя, команды, компании, какой бы ни была модель. Сначала рассмотрим основы определения поддоменов в маршрутах Laravel, а затем перейдём к практическим действиям.

Прежде чем начать

Чтобы экспериментировать с поддоменами локально, ваша система разработки должна поддерживать поддомены. Я использую Laravel Herd, поэтому мы будем работать с проектом под названием subdomains, доступным в браузере по адресу subdomains.test. Laravel Valet также будет работать с поддоменами по умолчанию.

Если используете какое-либо другое решение, может потребоваться настройка nginx или другого сервера, который используется, чтобы это работало.

Захват поддоменов

Чтобы захватить маршруты поддоменов в Laravel, создайте группу маршрутов и укажите домен.

Route::domain('{subdomain}.subdomains.test')->group(function () {
// все сгруппированные маршруты поддоменов находятся здесь
});

Этот новый маршрут будет отвечать любому поддомену. Все маршруты поддоменов будут находиться внутри группы. Например.

Route::domain('{subdomain}.subdomains.test')->group(function () {
Route::get('/', function (string $subdomain) {
dd($subdomain);
});
});

Этот новый маршрут, представляющий домашнюю страницу поддомена, получает переданный в него $subdomain. Все маршруты, зарегистрированные в этой группе, получат то же самое.

При обращении к маршруту / будет выгружено всё, что было передано в качестве поддомена. Если я получу доступ к alex.subdomains.test, то будет выведен в дамп alex. Довольно просто!

Соответствие домена

Сейчас мы жёстко закодируем основной домен приложения в маршруте. Это не идеальный вариант, для продакшена, потому что домен изменится. Не беспокойтесь, получите доступ к домену, над которым работаете, из конфигурации и замените его.

Route::domain('{subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->group(function () {
Route::get('/', function (string $subdomain) {
dd($subdomain);
});
});

Существует множество способов сделать это, но данное решение заменяет https:// и http:// из URL, заданного в config/app.php, так что остаётся чистый URL, который нужно добавить в конец группы поддоменов.

Получение моделей из поддомена

Давайте сделаем это более удобным, добавив в таблицу users столбец subdomain и получим пользователя по его поддомену.

php artisan make:migration add_subdomain_to_users_table

Затем в методе миграции up необходимо добавить столбец subdomain, обеспечив его уникальность.

public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('subdomain', 20)->unique();
});
}

Запустите миграцию, создайте пользователя в базе данных и установите поддомен.

После того как пользователь будет внесён в базу данных, обновите маршрут, чтобы он выглядел следующим образом.

Route::domain('{user:subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->group(function () {
Route::get('/', function (User $user) {
dd($user);
});
});

Теперь этот маршрут использует привязку модели маршрута для получения пользователя по колонке subdomain. Любые маршруты, определённые внутри этой группы, теперь могут получить экземпляр User и использовать его в маршруте. Запуск, приведённого выше маршрута, приведёт к дампу найденного пользователя и, что очень важно, к 404, если пользователь не был найден.

Поздравляю, теперь в вашем приложении успешно работает динамическое получение пользователей (или любой другой модели) с помощью поддомена!

Организация маршрутов и контроллеров поддоменов

Вряд ли вы будете использовать замыкание маршрутов, поэтому давайте поговорим об организации контроллеров.

Мне нравится хранить все контроллеры, связанные с сопоставлением поддоменов, в отдельной директории. Например, может быть контроллер HomeController для актуальной домашней страницы приложения, но у каждого пользовательского поддомена также будет свой контроллер HomeController.

Создадим поддомен HomeController и наведём в нём порядок.

php artisan make:controller Subdomain\\HomeController

Упрощая задачу, по-прежнему будем просто выводить пользователя.

class HomeController extends Controller
{
public function __invoke(User $user)
{
dump($user);
}
}

А затем приведём в порядок маршруты, чтобы они были аккуратнее.

Route::domain('{user:subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->group(function () {
Route::get('/', HomeController::class);
});

Используя тот же шаблон, сохраним все контроллеры, связанные с поддоменом приложения, в одном месте.

Дополнительная привязка маршрута к модели

Мы уже используем привязку маршрутной модели для разрешения пользователя, но что, если потребуется продолжить привязку маршрутов к модели, для получения вещей, относящихся к этому пользователю?

На примере Article для каждого пользователя создадим модель и миграцию.

php artisan make:model Article -m

Заполним определение схемы и выполним миграцию.

Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('title');
$table->string('slug')->unique();
$table->timestamps();
});

Наконец, необходимо настроить отношения между пользователями и статьями.

Свяжем модель User со статьями.

public function articles()
{
return $this->hasMany(Article::class);
}

А модель Article свяжем с пользователем.

public function user()
{
return $this->belongsTo(User::class);
}

Создадим тестовую статью в базе данных (или используем seeder), чтобы можно было поэкспериментировать с доступом к статьям на поддомене.

Создадим новый маршрут и контроллер для работы со статьями, доступными на поддомене.

Route::domain('{user:subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->group(function () {
Route::get('/', HomeController::class);
Route::get('/{article:slug}', ArticleController::class);
});

И контроллер ArticleController.

class ArticleController extends Controller
{
public function __invoke(User $user, Article $article)
{
dd($article);
}
}

Если создадим в базе данных статью со slug an-article и посетим alex.subdomains.test/an-article, то увидим, дамп статьи.

Ещё лучше то, что статья, которую мы передаём, автоматически привязывается к пользователю. Это означает, что мы не можем передать статью в поддомене Alex, которая ему не принадлежит.

Генерация маршрутов для поддоменов

Скорее всего, понадобятся внутренние ссылки на разные страницы внутри поддомена. Как создать маршруты для этого?

Сначала создадим несколько представлений для двух имеющихся маршрутов и передадим список статей, принадлежащих пользователю поддомена, чтобы можно было кликнуть и прочитать их.

Вот новый контроллер поддомена HomeController.

class HomeController extends Controller
{
public function __invoke(User $user)
{
return view('subdomain.home', [
'user' => $user,
'articles' => $user->articles()->latest()->get(),
]);
}
}

И представление home.blade.php (опять, в отдельном каталоге).

<div>
<h1>Articles by {{ $user->name }}</h1>

@foreach($articles as $article)
<div>
<a href="">{{ $article->title }}</a>
</div>
@endforeach
</div>

Довольно просто, но теперь необходимо установить связь. Ключевым моментом является установка определённых имён для каждого уровня маршрутов. Вот как выглядит маршрут поддомена после внесения изменений.

Route::domain('{user:subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->name('subdomain.')
->group(function () {
Route::get('/', HomeController::class);
Route::get('/{article:slug}', ArticleController::class)
->name('article');
});

Присваиваем общему домену имя subdomain. (обратите внимание на лишнюю точку), а затем каждому маршруту внутри стандартное имя.

Это означает, что доступ к имени маршрута можно получить следующим образом.

@foreach($articles as $article)
<div>
<a href="{{ route('subdomain.article', ['user' => $user, 'article' => $article]) }}">{{ $article->title }}</a>
</div>
@endforeach

Теперь, когда всё это добавлено, можно ссылаться на любой маршрут поддомена для любого пользователя.

Вы освоили поддомены

Мы рассмотрели основные принципы работы с поддоменами в Laravel, и теперь вы можете добавлять любые дополнительные маршруты в группу поддоменов, получать доступ к любым данным в этой модели, а также создавать ссылки внутри приложения на поддомены (в том числе внутренние).

Дополнительные материалы

Предыдущая Статья

Основы каскада и специфичности

Следующая Статья

Наследование в CSS