Implementing mutually exclusive roles on Spatie Laravel-permission with Nova
Working on a project that implemented Spatie\Laravel-permission to manage user roles and permissions, we’ve found a situation where users could have two roles that (based on the business logic) shouldn’t be assigned at the same time (e.g. in some businesses, due to legal regulations, employees cannot be customers).
To achieve this constraint, we needed to make some changes to our implementation, and here are the changes we’ve made:
Step 1: Adding mutually exclusive pairs of roles
We’ll add a new configuration variable to the permissions.php
file under config
directory.
This configuration variable will store in the form of an array, pairs of roles that shouldn’t be assigned at the same time.
// config/permissions.php
/**
* You can define here all the pair of roles that should be considered mutually exclusive.
* When a user is assigned with both roles, an Exception will be thrown.
*/
'mutually_exclusive_roles' => [
['employee', 'customer'],
['owner', 'customer']
],
Step 2: Modifying the User model
Now, we need to override the syncRoles
function of the HasRoles
trait to make it capable of detecting when the user is trying to assign conflictive roles and throwing an exception.
// app/Models/User.php
use Spatie\Permission\Traits\HasRoles;
class User extends Model {
use HasRoles {
syncRoles as protected traitSyncRoles;
}
// ...
public function syncRoles(...$roles)
{
$exclusiveRoles = config('permission.mutually_exclusive_roles');
// Let's verify if the roles aren't mutually exclusive
foreach ($exclusiveRoles as $pair) {
// if the intersection between full role list and the pair are equal to the pair, that means
// there's a role conflict.
if (count(array_intersect($roles, $pair)) === count($pair)) {
throw new Exception(
Str::ucfirst(implode(' and ', $pair)).' roles cannot be assigned at the same time.',
400
);
}
}
return $this->traitSyncRoles(...$roles);
}
}
Step 3: Adding fillusing to our Laravel Nova field
In our project, we’ve implemented the role selection using the ziffmedia/nova-select-plus field, you must adapt this to your own implementation, but the key here is to resolve the role with the function we’ve overridden.
// app/Nova/User.php
class User extends Resource
{
// ...
public function fields(NovaRequest $request)
{
return [
// ...
SelectPlus::make('Role', 'roles', Role::class)
->fillUsing(function ($request) {
// List all the currently selected roles
$decodedRoles = json_decode($request->roles, true);
$selectedRoles = array_column($decodedRoles, 'label');
$this->syncRoles(...$selectedRoles);
})
->label('name')
->usingIndexLabel('name')
->usingDetailLabel('name'),
];
}
// ...
}
Now, when you assign two roles that are mutually exclusive, you’ll see an error like the following