How to implement a basic, role-based, ACL in Laravel
Tags: laravel
Published: 2020-06-23 09:16:29

---
title: How to implement a basic, role-based, ACL in Laravel
published: true
description: 
tags: laravel
---

## 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'])`.

```PHP
// 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):

```PHP
// 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:
```PHP
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:
```PHP
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:
```PHP
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
```PHP
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:
```PHP
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`:
```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:
```PHP
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`:

```PHP
protected $routeMiddleware = [
    'role' => \App\Http\Middleware\Role::class,
];
```