Простое управление ролям в Laravel с помощью Pivot

Большинству из нас не нужна сложная система управления доступов в наших приложениях. Мы хотим простую и гибкую систему, без излишеств. Что мы подразумеваем под «простой» системой? Допустим у вас в проекте есть команды, у команд есть владелец и участники. Мы хотим знать: кто владелец, у кого есть раширенные права, а кто простой член команды.

Говоря на языке «кода», это значит что у нас есть модели User и Team, а так же промежуточная таблица, которая их объеденяет. Далее я покажу вам просто вариант подобной связи.

Промежуточная таблица

Структура промежуточной таблицы проста и понятна. В общем случае она выглядит примерно так:

Schema::create('team_user', function (Blueprint $table) {
  $table->unsignedInteger('team_id')->index();
  $table->unsignedInteger('user_id')->index();
  $table->string('role')->default('member');
  $table->unique(['team_id', 'user_id']);
// Foreign constraints, timestamps etc..
});

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

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

Настраиваем Eloquent связи

Наш тип связи называется "many-to-many". Код ниже говорит Laravel о том, что мы хотим использовать many-to-many связь под названием team:

{
  return $this->belongsToMany(Team::class, 'team_user')
    ->using(Role::class)
    ->as('role')
    ->withPivot('role');
}

Это стандартное объявление belongsToMany() связи, в котором мы так же запрашиваем дополнительные поля из связующей таблицы с помощью withPivot() метода.

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

Кроме стандартных методов, мы используем еще две не совсем стандартные вещи. Первая: использование метода using(), который говорит Laravel о том, что мы хотим использовать модель Role для нашей связи, вместо стандартного класса Illuminate\Database\Eloquent\Relations\Pivot.

Вторая вещь: использование as() метода, который позволяет переименовать стандартный ключ pivot. Т.е теперь мы можем делать так:

// default
$user->team->pivot;

// with the as('role')
$user->team->role;

Такой подход делает код более понятным и читаемым.

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

После того, как мы объявили связь, мы можем добавить пользователя в команду. Обратите внимание что мы объявили связь только со стороны User, но если вы хотите сделать обратную связь (со стороны Team), вы можете просто скопировать код выше в класс Team и заменить название модели.

И теперь, если верить документации, мы можем делать так:

// the user is an owner
$user->teams()->attach(1, ['role' => 'owner']);

// omit the role to use the default "member"
$user->teams()->attach(1);

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

Генерация модели для связующей таблицы

Мало кто знает, но в Laravel можно сгенерировать модель для связующей таблицы с помощью Artisan! Все что необходимо сделать, это вызывать команду make:model с параметром -p:

$ php artisan make:model Role -p

И теперь мы можем делать все что захотим. Мы можем работать с этой моделью, как с обычной Eloquent моделью.

Добавляем функционал в нашу pivot модель

Добавим в нашу Role модель проверку состоит ли пользователь в одной из групп:

public function hasPermission($roles)
{
  return in_array($this->role, (array) $roles);
}

Использовать этот медот можно например так:

$user->teams->first()->role->hasPermission('owner'); // false

$user->teams->first()->role->hasPermission(['owner', 'member']); // true

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

Так же мы можем легко добавить перевод для роли пользователя:

protected $appends = [
  'label',
];

public function getLabelAttribute()
{
  return trans('roles'.$this->role);
}

Заключение

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