DEV Community

Kamran Khan
Kamran Khan

Posted on

sometimes, unique-except-self: the small validation rules that make admin edits safe

Admin edit forms have a quiet failure mode: you open a user's record, change nothing except the role, hit save — and the API rejects you because the unchanged email "has already been taken". By you. The user you're editing. Every admin panel hits this in week one; the fix is two FormRequest rules most people learn the hard way.

sometimes: validate what's there, ignore what's not

The update request for users looks like this:

public function rules(): array
{
    return [
        'name'     => ['sometimes', 'string', 'max:255'],
        'email'    => ['sometimes', 'string', 'email', 'max:255',
                        Rule::unique('users', 'email')->ignore($this->route('user'))],
        'password' => ['sometimes', 'string', 'min:8'],
        'role'     => ['sometimes', 'string', 'in:admin,user'],
    ];
}
Enter fullscreen mode Exit fullscreen mode

sometimes means: only run these rules if the field is present in the request. Send {role: "admin"} and the name/email/password rules never fire — absent fields pass through untouched instead of failing required or, worse, being nulled out by $request->validated().

This is the difference between partial updates done right and the classic bug where an edit wipes fields the form didn't send. The controller is one line — $user->update($request->validated()) — and it's safe precisely because validated() only contains what was sent and passed.

unique-except-self: the email that "already exists"

The email rule is the one I see copied wrong most often:

Rule::unique('users', 'email')->ignore($this->route('user'))
Enter fullscreen mode Exit fullscreen mode

Plain unique:users,email compares against every row, including the one being edited — so saving an unchanged email fails validation. ->ignore() exempts the current record. $this->route('user') is the route-model-bound User instance (the users/{user} resource route provides it), and ignore() accepts the model directly — no need to pluck the id yourself.

Small detail, big consequence: without it, admins learn to "fix" the error by tweaking the email slightly. Then the user's login breaks. I've watched this happen.

Contrast with the create request

The store request is the strict sibling — everything required, because creation has no existing values to fall back on:

'name'     => ['required', 'string', 'max:255'],
'email'    => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8'],
'role'     => ['required', 'string', 'in:admin,user'],
Enter fullscreen mode Exit fullscreen mode

Two deliberate asymmetries worth stealing:

  • role is required on create but sometimes on update. A new user must have an explicit role; an edit shouldn't have to restate it. (And role on the store request is safe here only because the entire users resource sits behind role:admin middleware — an admin assigning roles is the feature.)
  • Registration's password needs confirmed; the admin's doesn't. RegisterRequest has 'password' => ['required', 'string', 'min:8', 'confirmed'] — a self-service stranger must type it twice. An admin setting a password for someone else gets no confirmation field, because there is no second typing to compare against. Match the rule to the trust level of the actor.

None of these rules is clever. Together they're the difference between an admin panel you trust and one where every edit feels slightly dangerous. Validate what's sent, exempt the record you're editing, and let the request shape — create vs update — decide how strict each field is.


These are the exact FormRequests in my Laravel + React SaaS Starter Kit ($19 one-time) — Sanctum auth, role-based access, and an admin users table with validation that doesn't fight you.

Top comments (0)