Implementing User Roles into a Laravel Application

This Article was written around Laravel 5.6 and may not work with Laravel 6, 7 or 8

User Roles allow us to control access to a page or feature within an application.

I recently implemented User Roles into a project to control access in the staff section of a website for different user groups; admin, management, support, accounts mangers for example.

This approach was inspired by how its done in Symfony. It is one way you can do it, but it depends on how it needs to fit into your project. For what I needed it for I didn’t need to store the available roles in a database, but kept it in a class.

So lets dive in!

Implementing User Roles

First we need to add a new field to the user table to store what roles they have. I’m assuming you already have a users table. So we’ll create a migration to do that:

php artisan make:migration add_roles_to_users_table --table=users

And then we’ll add a text column which can be nullable (users don’t need to have any roles):

<?php

use
Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddRolesToUsersTable extends Migration
{
/**
* Run the migrations.
*
*
@return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->text('roles')->nullable();
});
}

/**
* Reverse the migrations.
*
*
@return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('roles');
});
}
}

Now we need to update our User model. First we’ll add cast for the table column to cast it to/from an array.

/**
*
@var array
*/
protected $casts = [
'roles' => 'array',
];

Now we need the roles methods. These methods will allow us to get, check and add roles for the user:

/***
*
@param string $role
*
@return $this
*/
public function addRole(string $role)
{
$roles = $this->getRoles();
$roles[] = $role;

$roles = array_unique($roles);
$this->setRoles($roles);

return $this;
}

/**
*
@param array $roles
*
@return $this
*/
public function setRoles(array $roles)
{
$this->setAttribute('roles', $roles);
return $this;
}

/***
*
@param $role
*
@return mixed
*/
public function hasRole($role)
{
return in_array($role, $this->getRoles());
}

/***
*
@param $roles
*
@return mixed
*/
public function hasRoles($roles)
{
$currentRoles = $this->getRoles();
foreach($roles as $role) {
if ( ! in_array($role, $currentRoles )) {
return false;
}
}
return true;
}

/**
*
@return array
*/
public function getRoles()
{
$roles = $this->getAttribute('roles');

if (is_null($roles)) {
$roles = [];
}

return $roles;
}

The way Symfony does User Roles is that they have a hierarchy, so some roles can have other lower roles.

Using that idea I created a UserRole class to contain what roles a User could have and their hierarchy.

<?php

namespace
App\Role;

/***
* Class UserRole
*
@package App\Role
*/
class UserRole {

const ROLE_ADMIN = 'ROLE_ADMIN';
const ROLE_MANAGEMENT = 'ROLE_MANAGEMENT';
const ROLE_FINANCE = 'ROLE_FINANCE';
const ROLE_ACCOUNT_MANAGER = 'ROLE_ACCOUNT_MANAGER';
const ROLE_SUPPORT = 'ROLE_SUPPORT';

/**
*
@var array
*/
protected static $roleHierarchy = [
self::ROLE_ADMIN => ['*'],
self::ROLE_MANAGEMENT => [
self::ROLE_ACCOUNT_MANAGER,
self::ROLE_FINANCE,
self::ROLE_SUPPORT,
],
self::ROLE_ACCOUNT_MANAGER => [
self::ROLE_SUPPORT
],
self::ROLE_FINANCE => [
self::ROLE_SUPPORT
],
self::ROLE_SUPPORT => []
];

/**
*
@param string $role
*
@return array
*/
public static function getAllowedRoles(string $role)
{
if (isset(self::$roleHierarchy[$role])) {
return self::$roleHierarchy[$role];
}

return [];
}

/***
*
@return array
*/
public static function getRoleList()
{
return [
static::ROLE_ADMIN =>'Admin',
static::ROLE_MANAGEMENT => 'Management',
static::ROLE_ACCOUNT_MANAGER => 'Account Manager',
static::ROLE_FINANCE => 'Finance',
static::ROLE_SUPPORT => 'Support',
];
}

}

For my use case I currently have 5 user roles, but you can have as many as you want depending on your project.

Now that we have updated the User and have a UserRole class, we need to create a RoleChecker. We need this because of the role hierarchy, as the admin role has every other role we can’t check this using the User::hasRole() method. A role checker should have that responsibility.

The RoleChecker class is quite simple:

<?php

namespace
App\Role;

use App\User;

/**
* Class RoleChecker
*
@package App\Role
*/
class RoleChecker
{
/**
*
@param User $user
*
@param string $role
*
@return bool
*/
public function check(User $user, string $role)
{
// Admin has everything
if ($user->hasRole(UserRole::ROLE_ADMIN)) {
return true;
}
else if($user->hasRole(UserRole::ROLE_MANAGEMENT)) {
$managementRoles = UserRole::getAllowedRoles(UserRole::ROLE_MANAGEMENT);

if (in_array($role, $managementRoles)) {
return true;
}
}

return $user->hasRole($role);
}
}

If the user has the admin role, they can do anything. Then we check if the user has management role and if that role if covered in the role hierarchy. Otherwise we return if the user has the role.

Next we need to create a Middleware for the role checking so we can use it either globally or on specific route and route groups.

So we’ll run the following artisan command to create our middleware:

php artisan make:middleware CheckUserRole

This will put a new file in app/Http/Middleware and will look something like this:

<?php

namespace
App\Http\Middleware;

use Closure;

class CheckUserRole
{
/**
* Handle an incoming request.
*
*
@param \Illuminate\Http\Request $request
*
@param \Closure $next
*
@return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
}

We need to inject in the role checker we created above and then use it. Your Middleware will now look like this:

<?php

namespace
App\Http\Middleware;

use Closure;
use App\User;
use App\Role\RoleChecker;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Auth;

/**
* Class CheckUserRole
*
@package App\Http\Middleware
*/
class CheckUserRole
{
/**
*
@var RoleChecker
*/
protected $roleChecker;

public function __construct(RoleChecker $roleChecker)
{
$this->roleChecker = $roleChecker;
}

/**
* Handle an incoming request.
*
*
@param \Illuminate\Http\Request $request
*
@param \Closure $next
*
@param string $role
*
@return mixed
*
@throws AuthorizationException
*/
public function handle($request, Closure $next, $role)
{
/** @var User $user */
$user = Auth::guard()->user();

if ( ! $this->roleChecker->check($user, $role)) {
throw new AuthorizationException('You do not have permission to view this page');
}

return $next($request);
}
}

So in the middleware we’re checking if the user has a role relevant to route/request and if not its throwing an AuthorizationException.

Now we need to register the middleware in our service provider. You can use one that comes with the default Laravel install e.g AppServiceProvider in the apps/Providers directory

$this->app->singleton(CheckUserRole::class, function(Application $app) {
return new CheckUserRole(
$app->make(RoleChecker::class)
);
});

Now we need to register the middleware in the Kernel, located at app\Http\Kernel.php. It needs to go in the $routeMiddleware section. I’ve omitted the other entries for readability:

/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
*
@var array
*/
protected $routeMiddleware = [
'check_user_role' => \App\Http\Middleware\CheckUserRole::class,
];

Now we have our middleware setup we can use it. We’ll create a route inside admin that uses the middleware check_user_role and requires the user role finance

Route::get('admin/finance', function () {
//
})->middleware('check_user_role:' . \App\Role\UserRole::ROLE_FINANCE);

And there we have it, a nice way of setting up user roles, a role checker and middleware. This implementation could be taken one step further by putting the roles in a database for larger systems where this needs to change without development.

PHP and JavaScript hacker. Symfony and Laravel tinkerer. Creator of TomahawkPHP

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store