Админ-панель в Laravel 5 – миграции, модели, middleware и роуты

Админ-панель в Laravel 5 – миграции, модели, middleware и роуты

Если говорить о сайтах, то практически всегда есть группа пользователей или, как минимум, один пользователь с особыми правами – админ. Давайте посмотрим, как можно ограничить доступ к админ-панели в Laravel.

Есть несколько вариантов реализации. Отличаются они только тем, будем ли мы использовать роли, и сколько ролей может быть у пользователя (одна или больше). Во всех случаях порядок действий одинаков:

  1. Редактируем или создаём необходимые миграции и выполняем их.
  2. Создаём модели и, если того требует ситуация, описываем отношения между ними.
  3. Создаём и регистрируем middleware.
  4. Настраиваем роуты.

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

Вариант 1. Дополнительное поле is_admin

Самая простая реализация. Если сайт небольшой, то другой и не нужно.

Миграции

Эта версия не подразумевает никаких дополнительных таблиц / моделей / отношений. Нужно всего лишь добавить дополнительное поле is_admin в файл миграции пользователей. Миграция уже есть «в коробке», поэтому открываем файл project/app/database/migrations/XXXX_XX_XX_create_users_table.php и редактируем. Код должен выглядеть так:

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->boolean('is_admin')->default(0);
            $table->rememberToken();
            $table->timestamps();
        });
    }

    ...
}

Теперь открываем консоль и переходим в корень проекта. После чего запускаем миграции:

$ php artisan migrate
Модель

Модель User создавать не нужно, так как она также есть по умолчанию (project/app/User.php). Но нужно выполнить два действия. Во-первых, для того, чтобы не возникало ошибок при массовом заполнении (mass assignments), добавим атрибут is_admin в массив $fillable. Во-вторых, добавим метод isAdmin(), который будет возвращать true или false, в зависимости от того, является ли пользователь админом. Вот код модели:

class User extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password', 'is_admin'
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    public function isAdmin()
    {
        return $this->is_admin;
    }
}

 

Вариант 2. Роли. Отношение «один ко многим»

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

Миграции

Поскольку нам понадобится и таблица, и модель ролей, создадим их одной командой в консоли:

$ php artisan make:migration -m Role

Не будем делать чего-то слишком сложного и добавим в файл миграции всего два поля id и name. Другими словами, файл миграции должен выглядеть так:

<?php

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

class CreateRolesTable extends Migration
{
    public function up()
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->tinyIncrements('id');
            $table->string('name')->unique();
        });
    }

    public function down()
    {
        Schema::dropIfExists('roles');
    }
}

Также нужно добавить поле role_id в таблицу пользователей, поэтому открываем соответствующий файл миграции и делаем это:

class CreateUsersTable extends Migration
{

    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->tinyInteger('role_id')->unsigned();
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();

            $table->foreign('role_id')->references('id')->on('roles');
        });
    }

    ...
}

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

$ php artisan migrate
Модели

Что нужно прописать в модели Role? В массив $fillable всего одно поле, которое можно заполнять, нужно указать, что отсутствуют даты создания и обновления ($timestamps). Кроме этого, опишем отношения между с моделью User в методе users(), который указывает на то, что у одной роли может быть много пользователей:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use App\User;

class Role extends Model
{
    protected $fillable = ['name'];

    public $timestamps = false;

    public function users()
    {
        return $this->hasMany(User::class);
    }
}

В свою очередь, в модели пользователей допишем атрибут role_id в массив $fillable. Также определим и обратное отношение в методе role() и снова напишем метод isAdmin() с учётом того, что теперь используются роли:

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Role;

class User extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password', 'role_id',
    ];


    protected $hidden = [
        'password', 'remember_token',
    ];

    public function role()
    {
        return $this->belongsTo(Role::class);
    }

    public function isAdmin()
    {
        return $this->role->name == 'admin';
    }
}

 

Вариант 3. Роли. Многие ко многим

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

Миграции

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

Миграция ролей точно такая же, как в предыдущем варианте, поэтому перейдём сразу к промежуточной role_user. В этом случае модель не понадобится, поэтому создаём только миграцию командой:

$ php artisan make:migration create_role_user_table --create=role_user

Открываем файл и вставляем следующий код:

<?php

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

class CreateRoleUserTable extends Migration
{
    public function up()
    {
        Schema::create('role_user', function (Blueprint $table) {
            $table->tinyInteger('role_id')->unsigned();
            $table->integer('user_id')->unsigned();

            $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');

            $table->primary(['user_id', 'role_id']);
        });
    }

    public function down()
    {
        Schema::dropIfExists('role_user');
    }
}

После этого вновь запускаем создание таблиц в базе данных:

$ php artisan migrate
Модели

В целом модели сходны со вторым вариантом, но теперь в обеих таблицах будем использовать belongsToMany, и, в модели пользователей метод isAdmin() снова будет несколько иным. Модель роли:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use App\User;

class Role extends Model
{
    protected $fillable = ['name'];

    public $timestamps = false;

    public function users()
    {
    	return $this->belongsToMany(User::class);
    }
}

Модель пользователя:

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Role;

class User extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];


    protected $hidden = [
        'password', 'remember_token',
    ];

    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }

    public function isAdmin()
    {
        return (boolean)$this->roles->where('name', 'admin')->count();
    }
}

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

return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');

К слову, нелишним будет почитать документацию, поскольку отношение «многие ко многим» рассматривается как раз на примере пользователей и ролей.

Заполнение таблицы ролей

Если вы решили использовать второй или третий варианты, то перед созданием пользователей надо в таблице создать несколько ролей. Опять-таки, сделать это можно по-разному – использовать seeder или tinker, или попросту создать записи прямо в БД. Сейчас это не столь важно, так как речь в данном посте не об этом. Используем консоль и tinker:

$ php artisan tinker
>>> App\Role::create(['name' => 'admin']);
>>> App\Role::create(['name' => 'registered']);

Т.е., в итоге в таблице ролей у нас есть две записи.

Middleware

Зачем нужен middleware? Это некий слой, который будет выполнен, прежде чем выполнится запрос (или иные действия). В нашем случае middleware одинаков для всех вышеописанных вариантов и будет проверять, является ли пользователь админом. Если да – управление передаётся дальше, если нет – перенаправляет пользователя туда, куда мы скажем.

Создаём middleware командой в консоли:

$ php artisan make:middleware CheckIfAdmin

Путь к только что созданному файлу – project/app/Http/Middleware/CheckIfAdmin.php. Открываем его и вставляем следующий код:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class CheckIfAdmin
{
    public function handle($request, Closure $next)
    {
        if (Auth::check() && Auth::user()->isAdmin()) {
            return $next($request);
        }

        return redirect('admin/login');
    }
}

Думаю, код и так прекрасно читается, но на всякий случай уточню – мы проверяем, авторизован ли пользователь и если да, то является ли он админом. Если оба условия верны, как и было обещано, выполняется HTTP-запрос, если нет – отправляем пользователя на страницу логина админ-панели.

Замечу, что вовсе не обязательно использовать фасад Auth или хелпер auth(), поскольку Laravel предоставляет данные о пользователе и в реквесте. Кроме того, можно немного изменить логику. Например, если пользователь не авторизован, будем отправлять его на страницу логина админ-панели. Если же он авторизован, но не является админом – отправим его на главную страницу сайта. Т.е. код будет выглядеть так:

<?php

namespace App\Http\Middleware;

use Closure;

class CheckIfAdmin
{
    public function handle($request, Closure $next)
    {
        $user = $request->user();

        if (!isset($user)) {
            return redirect('admin/login');
        }

        if (!$user->isAdmin()) {
            return redirect('/');
        }

        return $next($request);
    }
}

Какой вариант выбрать – решать вам. Сути это не меняет и оба будут прекрасно работать. Теперь надо зарегистрировать наш middleware. Для этого открываем файл project/app/Http/Kernel.php, находим массив $routedMiddleware и добавляем наш класс:

class Kernel extends HttpKernel
{
    ...
    
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'admin' => \App\Http\Middleware\CheckIfAdmin::class,
    ];
}

 

Роуты

Прежде чем приступить к роутам, подумаем вот о чём: фактически приложение будет включать два подприложения – непосредственно сам сайт и админ-панель. А значит, есть смысл разбить и роуты на две группы. Также можно сгруппировать и контроллеры – в папке app/Http/Controllers создадим две директории. В первой – Admin – будут контроллеры для админ-панели (например, DashboardController), а во второй – Site – контроллеры для фронтенда (например, HomeController). При этом заметим, что во всех адресах админки после имени сайта будет присутствовать префикс admin. Т.е. есть смысл сгруппировать роуты по префиксам и неймспейсам. Также при каждом запросе проверять, является ли пользователь админом. За исключением одного случая – входа в админку, т.е. когда пользователь ещё не авторизован. С учётом всего сказанного напишем такие роуты (в файле project/app/routes/web.php):

// Admin routes
Route::group(['prefix' => 'admin', 'namespace' => 'Admin'], function() {
	Route::get('login', 'LoginController@index');
	Route::post('login', 'LoginController@login')->name('admin.login');

	Route::group(['middleware' => 'admin'], function() {
		Route::get('/', 'DashboardController')->name('admin.dashboard');
	});
});

// Site routes
Route::group(['namespace' => 'Site'], function() {
	Route::get('/', 'HomeController@index')->name('homepage');
});

 

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