TL;DR
Note: roles will be created in database "on the go", through the Role::wrap
method. So there is no need to seed your database with roles to start using them. If you want to seed your database nonetheless with roles, use Role::fromArray(['admin', 'vip', 'guest'])
.
// on a route:
Route::get('admin', 'AdminController@index')->middleware('role:admin');
// give a role to an user:
$user->addRole('admin');
// remove role from an user:
$user->removeRole('admin');
// select users with `admin` role:
User::whereRole('admin')->get();
// get roles of an user:
$user->roles; // Collection
$user->roles(); // Query Builder
Tell if an user has role(s):
// true if user has 'admin' role, false otherwise.
$user->hasRole('admin');
// true if user has 'guest' or 'vip' (or both) roles, false otherwise.
$user->hasAnyRole('guest', 'vip');
// true if user has both 'admin' and 'accounting' roles, false otherwise.
$user->hasRoles('admin', 'accounting');
Implementation
1. Migrations
Run: php artisan make:migration create_roles_table --create=roles
Fill it with:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateRolesTable extends Migration
{
public function up()
{
Schema::create('roles', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name')->unique();
$table->string('description')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('roles');
}
}
Run: php artisan make:migration create_role_user_table --create=role_user
Fill it with:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateRoleUserTable extends Migration
{
public function up()
{
Schema::create('role_user', function (Blueprint $table) {
$table->bigIncrements('id');
$table->foreignId('user_id');
$table->foreignId('role_id');
$table->timestamps();
$table->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade')->onUpdate('cascade');
$table->foreign('role_id')
->references('id')->on('roles')
->onDelete('cascade')->onUpdate('cascade');
});
}
public function down()
{
Schema::dropIfExists('role_user');
}
}
2. Model & Traits
Run: php artisan make:model Role
Fill it with:
namespace App;
use App\HasName;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
use HasName;
protected $fillable = [
'name',
'description',
];
public function users()
{
return $this->belongsToMany(User::class);
}
}
Create a new file app/Concerns/HasName.php
Fill it with
namespace App;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
trait HasName
{
public static function findFromName(string $name): self
{
return self::whereName($name)->firstOrFail();
}
public static function findOrCreate(string $name, string $description = ""): self
{
try {
return self::findFromName($name);
} catch (ModelNotFoundException $e) {
return self::create(compact('name'));
}
}
public static function wrap($name): self
{
if (is_string($name)) {
$name = self::findOrCreate($name);
}
if (is_array($name)) {
$name = self::firstOrCreate($name);
}
if (! $name instanceof self) {
throw new \InvalidArgumentException("\$name should be string, array, or " . self::class);
}
return $name;
}
public static function fromArray(array $array): Collection
{
return (new Collection($array))->map(fn($item) => self::wrap($item));
}
}
Create a new file app/Concerns/HasRoles.php
Fill it with:
namespace App;
use App\Role;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
trait HasRoles
{
public function roles()
{
return $this->belongsToMany(Role::class);
}
public function hasRole($role): bool
{
return $this->roles()->get()->contains(Role::wrap($role));
}
public function hasRoles(...$roles): bool
{
foreach (Arr::flatten($roles) as $role) {
if (! $this->hasRole($role)) {
return false;
}
}
return true;
}
public function hasAnyRole(...$roles): bool
{
foreach (Arr::flatten($roles) as $role) {
if ($this->hasRole($role)) {
return true;
}
}
return false;
}
public function addRole($role): void
{
$this->roles()->attach(Role::wrap($role));
}
public function removeRole($role): void
{
$this->roles()->detach(Role::wrap($role));
}
public function scopeWhereRole(Builder $query, $role): Builder
{
return $query->whereHas('roles', function($query) use ($role) {
return $query->where(DB::raw('"roles"."id"'), Role::wrap($role)->id)
});
}
}
Add this to app/User.php
:
namespace App;
use App\Concerns;
class User
{
use Concerns\HasRoles;
}
3. Middleware
Create a new file app/Http/Middleware/Role.php
Fill it with:
namespace App\Http\Middleware;
use Illuminate\Auth\Access\AuthorizationException;
class Role
{
public function handle($request, \Closure $next)
{
$roles = array_slice(func_get_args(), 2);
if (! $request->user()->hasAnyRole($roles)) {
throw new AuthorizationException("You don't have the required role to access this resource.");
}
return $next($request);
}
}
Update app/Http/Kernel.php
:
protected $routeMiddleware = [
'role' => \App\Http\Middleware\Role::class,
];
Top comments (2)
Thanks Benjamin! Helps a lot. I do get this error when migrating the role_user table. Any thoughts?
Laravel 5.6: BadMethodCallException Illuminate\Database\Schema\Blueprint::foreignId does not exist.
Hello Eelco,
foreignId
is a method introduced in Laravel 7. For 5.6 you need to useunsignedBigInteger
. Here's the complete list laravel.com/docs/5.6/migrations#cr...