minaremonshaker's avatar

Validating Role Assignments via Form Request

I have a controller method that assigns roles to users. The request includes a string containing the roles to assign, and I need to validate that each specified role exists in the roles table using a form request. Currently, I created a custom validation rule that converts the string into an array, loops through each role, and checks if it exists in the database—failing if any do not. I’m wondering if there’s an alternative approach to achieve this validation without creating a custom rule, or if that’s the only viable solution.

  • validation custom rule
class UserHasRole implements ValidationRule
{


    public function __construct(protected int $user_id){}
    /**
     * Run the validation rule.
     *
     * @param Closure(string, ?string=): PotentiallyTranslatedString $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
         $roles = explode(",", $value);

         $user = \App\Models\User::find($this->user_id);

         foreach ($roles as $role){
             if($user->hasRole($role)){
                 $fail("user has role $role");
             }
         }

    }
}
  • form request
class AssignRoleToUserRequest extends FormRequest
{
    public function rules(): array
    {
        $user_id = $this->route()->parameter('user');

        return [
            "roles" => ['required', 'string', new UserHasRole($user_id),]
        ];
    }

    public function authorize(): bool
    {
        return true;
    }
}
  • controller method
    public function assign(AssignRoleToUserRequest $request, int $user_id)
    {

    }
0 likes
7 replies
LaryAI's avatar
Level 58

You don’t need a custom validation rule just to confirm that all submitted roles exist in the roles table (unless you want custom logic like the user already having the role). Laravel’s built-in validation can handle this easily if you structure your request so that roles is an array instead of a string.

Recommended Approach

Step 1: In your frontend or API client, send the roles as an array:

{
  "roles": ["admin", "editor"]
}

—or, if you must accept a comma-separated string, preprocess it in your FormRequest's prepareForValidation method to convert to an array.

Step 2: In your AssignRoleToUserRequest form request, use the exists rule:

public function rules(): array
{
    return [
        // 'roles' must be an array
        'roles' => ['required', 'array'],
        // each element in roles[] must exist in the roles table, 'name' column
        'roles.*' => ['required', 'string', 'exists:roles,name'],
    ];
}

Optional: If the incoming value is a comma-separated string, normalize it first:

protected function prepareForValidation()
{
    if (is_string($this->roles)) {
        $this->merge([
            'roles' => array_map('trim', explode(',', $this->roles))
        ]);
    }
}

Summary

  • Validate roles as an array, and each role exists in roles.name
  • Use Laravel’s built-in validation rules—no custom rule required
  • Normalize string input in prepareForValidation, if necessary

Example

class AssignRoleToUserRequest extends FormRequest
{
    protected function prepareForValidation()
    {
        if (is_string($this->roles)) {
            $this->merge([
                'roles' => array_map('trim', explode(',', $this->roles))
            ]);
        }
    }

    public function rules(): array
    {
        return [
            'roles' => ['required', 'array'],
            'roles.*' => ['required', 'string', 'exists:roles,name'],
        ];
    }

    public function authorize(): bool
    {
        return true;
    }
}

Conclusion:
You can use Laravel’s built-in array and exists rules to validate that every submitted role exists in the roles table—no custom rule is necessary unless you want to check for more (like whether the user already has a role).

Glukinho's avatar
Level 30

You should send roles as JSON array, not a string. It would make validation a bit simpler. I don't think Laravel has builtin rule "a user has role" so this is a custom rule anyway.

$user = \App\Models\User::find($this->user_id);
...
if($user->hasRole($role)){
...

This code will trigger error "attempting hasRole on null" if a user with given $user_id is not found. Either use User::findOrFail() instead of find() or, better, move this check from validation to controller to utilize route model binding (so you deal with already instantiated User model instead of integer user_id). It would save you a bit of coding also.

I'd say "a user has some role" is something more complex than validation, this is a sort of business rule, so moving it to controller/service seem fine to me.

Validation better suits something more basic like "this rule name exists in database and is valid" (through roles => array; roles.* => string|exists:roles,name rules)

minaremonshaker's avatar

Hi, I followed your suggestion and am now sending the roles as a JSON array. However, I am facing another issue related to customizing the validation messages. I need to include the actual invalid values from the submitted array in the error message. When one of the array values fails validation, the error message I receive looks like the default one, but I would like to override it using the value itself.

From the documentation, I found several approaches, such as using the index or position placeholders, or using Rule::forEach, but they still rely on indexes or positions. Is there a better approach to achieve this?

https://laravel.com/docs/12.x/validation#accessing-nested-array-data

https://laravel.com/docs/12.x/validation#error-message-indexes-and-positions

data sent :

{
	"roles": [
		"admin",
		"user",
		"manager"
	]

}

request :

class AssignRoleToUserRequest extends FormRequest
{

    public function rules(): array
    {

        return [
            "roles" => ['required', 'array'],
            "roles.*" => ['exists:roles,name']
        ];
    }

    public function messages(): array
    {
        return [
            "roles.*.exists" => "the roles array contains :attribute that are not exists in our database" ,
        ];
    }


    public function authorize(): bool
    {
        return true;
    }
}

validation error message

{
	"message": "the roles array contains roles.2 that are not exists in our database",
	"errors": {
		"roles.2": [
			"the roles array contains roles.2 that are not exists in our database"
		]
	}
}
minaremonshaker's avatar

the contains dose not works as expected , i solved the problem by adding :input to the custom message , the :input gets the value as expected

    public function messages(): array
    {
        return [
            "roles.*.exists" => "the role :input dose not exists in our database" ,
        ];
    }
Tray2's avatar

Use a multi select dropdown for the roles, or use a checkbox group. They both give you an array of roles.

1 like
minaremonshaker's avatar

Hi, Thank you for your reply. I just wanted to let you know that I’m currently working only on the API and haven’t created the UI yet. But you’re right — I can use a checkbox group when I build it.

Please or to participate in this conversation.