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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.