Несколько Laravel Guard использующие одну таблицу

Некоторые вещи в Laravel, на мой взгляд, сделаны довольно странно. Одна из них – это то, как предлагается реализовывать роли пользователей инструментами фреймворка. В гайде от разработчиков предлагается проверять роль пользователя в middleware: то есть мы сначала авторизуем пользователя, а только потом узнаем, имеет ли он соответствующий доступ. Для проверки каких-то флагов доступа это выглядит разумно, но для ролей – решение странное.

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

Тем не менее, в сети есть немало примеров, предлагающих решение на основе guard’ов:

Правда все эти примеры имеют фатальный недостаток: пользователи с разными ролями должны находится в разных таблицах. Довольно странно создавать 5 одинаковых таблиц для 5 разных ролей, особенно в случаях, когда роли кардинально не отличаются.

Поэтому я предлагаю другое решение, благо это не сложно сделать, по крайней мере в последних версиях.

Кастомный guard user provider

Функциональность guard делится на driver и provider. Последний нам и нужно кастомизировать.

Для начала добавим поле role в таблицу users, где и будут хранится роли.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('role', 100)->after('remember_token')->default('user');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('role');
        });
    }
};

Добавляем admin guard. Меняем driver user provider на свой: я назвал его eloquentWithRole. Поле role в конфиге – это значение, которое будет храниться в базе данных.

<?php

return [
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquentWithRole',
            'model' => App\Models\User::class,
            'role' => 'user'
        ],
        'admins' => [
            'driver' => 'eloquentWithRole',
            'model' => App\Models\User::class,
            'role' => 'admin'
        ],
    ],
]

И наконец, создаем свой провайдер на основе базового: добавляем поиск по полю role в запрос, который должен возвращать пользователя.

<?php

namespace App\Providers;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Auth::provider('eloquentWithRole', function (Application $app, array $config) {

            $userProvider = new EloquentUserProvider($app['hash'], $config['model']);
            $userProvider->withQuery(function ($query) use ($config) {
                $query->where('role', $config['role']);
            });
            return $userProvider;
        });
    }
}

Теперь можно использовать guard admin. Пример для авторизации

Auth::guard('admin')->attempt($credentials)