Updated for Laravel 6.0
Introduction
In this article, we'll learn how to implement & setup roles and permissions in Laravel. There are alot of packages which handle this stuff for you by just pulling them in via composer, setting them up and you're good to go. But what I feel is these packages often comes with too much juice which I don't really need.
What if you only need to setup a real simple roles and permissions setup for your project. with these packages you'll have less options to customize as per your needs. Some people often refer this as re-inventing the wheel, but as a matter of fact. Its not.
We'll try to cover almost every little thing for setting up roles and permissions. So Let's straight dive in and see what are we covering in this article.
- Database Structure & Migrations
- Relationships between Models
- Custom Directives for views
- Assigning roles and permissions to users
- Setting up Roles & Permissions middleware
There's so much to cover in this article, Let's start by a fresh Laravel install.
Setting Up
Open up your terminal and create a new Laravel project by typing in the following command
$ laravel new roles-permissions
DYI Config
Setup and config your database with your project and head over to the next step.
Making up our Authentication scaffolding
Lets start by making our authentication scaffolding:
$ php artisan make:auth
Models & Migrations
Start of by creating the required models and migrations for this project.
In the terminal, type in:
$ php artisan make:model Permission -m $ php artisan make:model Role -m
As you may know, -m flag will create a migration file for the model. Now you'll have two new migration files waiting for you to add new fields. Run the following command and migrate your database.
$ php artisan migrate
Edit the Permission migration file
Now, for the permissions table, we only need two fields, an id, a slug and a name. Let's add these in our migration file and our schema should look like
Schema::create('permissions', function (Blueprint $table) { $table->increments('id'); $table->string('slug'); //edit-posts $table->string('name'); // edit posts $table->timestamps(); });
Same for the Role migration file
We'll have the same set of fields for the roles table as well.
Schema::create('roles', function (Blueprint $table) { $table->increments('id'); $table->string('slug'); //web-developer $table->string('name'); //Web Developer $table->timestamps(); });
Adding pivot tables
As our understanding, we need to have the pivot tables for the following set of rules.
- User can have Permission
For this first pivot table, we'll create a new migration file for the table users_permissions
$ php artisan make:migration create_users_permissions_table --create=users_permissions
For this pivot table between users and permissions, our schema should look like
Schema::create('users_permissions', function (Blueprint $table) { $table->integer('user_id')->unsigned(); $table->integer('permission_id')->unsigned(); //FOREIGN KEY CONSTRAINTS $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade'); //SETTING THE PRIMARY KEYS $table->primary(['user_id','permission_id']); });
Here, we've set the foreign key constraints in order to delete the corresponding records if a user or permission is deleted. Primary keys for this table are user_id and permission_id.
- User can have Role
Now let's create a pivot table for users_roles.
$ php artisan make:migration create_users_roles_table --create=users_roles
The fields inside this table will pretty much the same as in users_permissions table. Our schema for this table will look like:
Schema::create('users_roles', function (Blueprint $table) { $table->integer('user_id')->unsigned(); $table->integer('role_id')->unsigned(); //FOREIGN KEY CONSTRAINTS $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); //SETTING THE PRIMARY KEYS $table->primary(['user_id','role_id']); });
- Under a particular Role, User may have specific Permission
For example, a user may have the permission for post a topic, and an admin may have the permission to edit or delete a topic. In this case, let's setup a new table for roles_permissions to handle this complexity.
$ php artisan make:migration create_roles_permissions_table --create=roles_permissions
The Schema will be like:
Schema::create('roles_permissions', function (Blueprint $table) { $table->integer('role_id')->unsigned(); $table->integer('permission_id')->unsigned(); //FOREIGN KEY CONSTRAINTS $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade'); //SETTING THE PRIMARY KEYS $table->primary(['role_id','permission_id']); });
If this doesn't start to make any sense to you just yet, well wait for us to build up the relationships between our tables. Things will get clear I promise.
Let's migrate every thing now.
$ php artisan migrate
Setting up the relationships
We'll start by creating the relationships between roles and permissions table. In our Role.php
//Role.php public function permissions() { return $this->belongsToMany(Permission::class,'roles_permissions'); }
Same goes for Permission.php in reverse.
//Permission.php public function roles() { return $this->belongsToMany(Role::class,'roles_permissions'); }
Now, in terms of User. A user has many roles. A user may have many permissions. But potentially a Role has many users and permission has many users. So we need to setup many to many relations in our User model. But what we'll do is we'll create a new Trait for this logic so that if we add another model in our project in the future, we can just pull it in.
Creating a Trait
Inside of our app directory, let's create a new directory and name it as Permissions and create a new file namely HasPermissionsTrait.php
<?php namespace App\Permissions; use App\Permission; use App\Role; trait HasPermissionsTrait { public function roles() { return $this->belongsToMany(Role::class,'users_roles'); } public function permissions() { return $this->belongsToMany(Permission::class,'users_permissions'); } }
A nice little trait has been setup to handle user relations. Back in our User model, just import this trait and we're good to go.
User hasRole
Now, we'll create a new function inside our HasPermissionsTrait.php and name it as hasRole as following
public function hasRole( ... $roles ) { foreach ($roles as $role) { if ($this->roles->contains('slug', $role)) { return true; } } return false; }
Here, we're iterating through the roles and checking by the slug field, if that specific role exists. You can check or debug this by using:
$user = $request->user(); //getting the current logged in user dd($user->hasRole('admin','editor')); // and so on
Checking Permissions
Now we need to build the ability to give a user some permissions. But wait, here we have a couple of conditions to tackle with:
- User may have individual Permission for some actions.
Back inside of HasPermissionsTrait.php, we will add some new methods for user permissions:
protected function hasPermissionTo($permission) { return $this->hasPermission($permission); } protected function hasPermission($permission) { return (bool) $this->permissions->where('slug', $permission->slug)->count(); }
We'll be utilizing the Laravel's "can" directive to check if the User have Permission. and instead of using $user->hasPermissionTo(), we'll use $user->can() To do so, we need to create a new PermissionsServiceProvider for authorization
$ php artisan make:provider PermissionsServiceProvider
Register your service provider and head over to the boot method to provide us a Gateway to use can() method.
//PermissionsServiceProvider.php public function boot() { Permission::get()->map(function($permission){ Gate::define($permission->slug, function($user) use ($permission){ return $user->hasPermissionTo($permission); }); }); }
Here, what we're doing is, mapping through all permissions, defining that permission slug (in our case) and finally checking if the user has permission. You can learn more about Laravel's Gate facade at Laravel's documentation. You can test it out as:
dd($user->can('permission-slug'));
- User may have Permission through a Role
To achieve this condition, in our HasPermissionsTrait.
//HasPermissionsTrait.php public function hasPermissionThroughRole($permission) { foreach ($permission->roles as $role){ if($this->roles->contains($role)) { return true; } } return false; }
Here, we're iterating through each permission associated with a role, remember we've a many to many relationship setup between roles and permissions table.
Now the hasPermissionTo() method will check between these two conditions.
//HasPermissionsTrait.php public function hasPermissionTo($permission) { return $this->hasPermissionThroughRole($permission) || $this->hasPermission($permission); }
Giving Permissions
Now let's say, we want to give a set of permissions to a logged in user, here's how we can achieve this
//HasPermissionsTrait.php public function givePermissionsTo(... $permissions) { $permissions = $this->getAllPermissions($permissions); dd($permissions); if($permissions === null) { return $this; } $this->permissions()->saveMany($permissions); return $this; }
Deleting Permissions
For deleting or removing permissions from the user scope, we can use the detach method.
//HasPermissionsTrait.php public function deletePermissions( ... $permissions ) { $permissions = $this->getAllPermissions($permissions); $this->permissions()->detach($permissions); return $this; }
This is pretty straight forward, same things will be applied for roles as well. So give it a try and leave us a comment on how you did it.
Add the Seeders
In this part, we'll create seeders for permissions, roles & users and will verify our set of methods accordingly. So let's start by creating seeders.
$ php artisan make:seeder PermissionTableSeeder $ php artisan make:seeder RoleTableSeeder $ php artisan make:seeder UserTableSeeder
Let's start by editing adding a couple of records in UserTableSeeder.php
//UserTableSeeder.php $dev_role = Role::where('slug','developer')->first(); $manager_role = Role::where('slug', 'manager')->first(); $dev_perm = Permission::where('slug','create-tasks')->first(); $manager_perm = Permission::where('slug','edit-users')->first(); $developer = new User(); $developer->name = 'Usama Muneer'; $developer->email = '[email protected]'; $developer->password = bcrypt('secret'); $developer->save(); $developer->roles()->attach($dev_role); $developer->permissions()->attach($dev_perm); $manager = new User(); $manager->name = 'Asad Butt'; $manager->email = '[email protected]'; $manager->password = bcrypt('secret'); $manager->save(); $manager->roles()->attach($manager_role); $manager->permissions()->attach($manager_perm);
Here, we're assigning the roles and permissions to our newly created users.
Head over to the next file, i.e RoleTableSeeder.php
$dev_permission = Permission::where('slug','create-tasks')->first(); $manager_permission = Permission::where('slug', 'edit-users')->first(); //RoleTableSeeder.php $dev_role = new Role(); $dev_role->slug = 'developer'; $dev_role->name = 'Front-end Developer'; $dev_role->save(); $dev_role->permissions()->attach($dev_permission); $manager_role = new Role(); $manager_role->slug = 'manager'; $manager_role->name = 'Assistant Manager'; $manager_role->save(); $manager_role->permissions()->attach($manager_permission);
Here, we're attaching the permissions to the roles, remember we've ManyToMany relationship between roles and permissions.
Last one is PermissionTableSeeder.php
//PermissionTableSeeder.php $dev_role = Role::where('slug','developer')->first(); $manager_role = Role::where('slug', 'manager')->first(); $createTasks = new Permission(); $createTasks->slug = 'create-tasks'; $createTasks->name = 'Create Tasks'; $createTasks->save(); $createTasks->roles()->attach($dev_role); $editUsers = new Permission(); $editUsers->slug = 'edit-users'; $editUsers->name = 'Edit Users'; $editUsers->save(); $editUsers->roles()->attach($manager_role);
Now that, all of our seeders are ready to go, lets run our migration with the --seed flag.
$ php artisan migrate:refresh --seed
To test this out in your routes files, we can die and dump on:
$user = $request->user();
dd($user->hasRole('developer')); //will return true, if user has role
dd($user->givePermissionsTo('create-tasks')); // will return permission, if not null
dd($user->can('create-tasks')); // will return true, if user has permission
Setting up the Custom Blade Directives
We can utilize the can directive inside of our blade files, which is Laravel's one of pre-defined method, which can be used for authorizing permissions for users,
Here we'll create a role directive to be available in our blade files for checking the roles for users.
Inside of the boot method in PermissionsServiceProvider.php
//PermissionsServiceProvider.php Blade::directive('role', function ($role){ return "<?php if(auth()->check() && auth()->user()->hasRole({$role})) :"; }); Blade::directive('endrole', function ($role){ return "<?php endif; ?>"; });
Inside of our view files, we can use it like:
@role('admin') <h1>Hello from the admin</h1> @endrole
Setup the Middleware
In order to protect our routes, we can setup the middleware to do so.
$ php artisan make:middleware RoleMiddleware
Add the middleware into your kernel & setup the handle method as follows
public function handle($request, Closure $next, $role, $permission = null) { if(!$request->user()->hasRole($role)) { abort(404); } if($permission !== null && !$request->user()->can($permission)) { abort(404); } return $next($request); }
Right in our routes, we can do something like this
Route::group(['middleware' => 'role:admin'], function() { Route::get('/admin', function() { return 'Welcome Admin'; }); });
Simple enough?
Final words
Github repo has been updated for Laravel 6.0.
Here's the working Github rep0 for this Laravel install. clone or download the files if you get stuck anywhere in between. Moreover try out these little tricks yourself and setup roles and permissions for your next Laravel application. Good luck. If you have any queries regarding this article, you may leave a comment below or you can also ask us on Twitter.