
Адмін-панель в Laravel 5 – міграції, моделі, middleware та роути
28.04.2018 16:27 | Laravel
Якщо мова йде про сайти, то практично завжди є група користувачів або ж, як мінімум, один користувач з особливими правами - адмін. Тож пропоную розглянути, як можна обмежити доступ до адмін-панелі в Laravel.
Є кілька варіантів реалізації. Відрізняються вони тільки тим, чи будемо ми використовувати ролі, і якщо так, то яка кількість ролей може бути у користувача (одна чи більше). У всіх випадках послідовність дій однакова:
- Редагуємо або створюємо необхідні міграції та виконуємо їх.
- Створюємо моделі і, якщо того потребує ситуація, оисуємо відношення між ними.
- Створюємо та реєструємо middleware.
- Налаштовуємо роути.
Треба зауважити, що відмінності будуть тільки в міграціях і моделях, а 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). При цьому зауважимо, що у всіх url-адресах адмінки після назви сайту буде присутній префікс 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');
});
Ось власне і все. Звичайно, можна було б навести ще парочку контролерів і уявлень, але, гадаю, з цим не так вже й складно розібратися. Головне - є основа, яку можна використовувати при створенні будь-яких сайтів.