skip to content

In Laravel, how do you validate that a username is unique on a profile-edit form without rejecting the user's own current username?

level: middleimportance: must knowfreq 72%

answer

  1. the row being edited is not a duplicate
  2. Rule::unique('users', 'username')
  3. ->ignore($user) or ->ignore($user->id)
  4. never take the ignored id from input
  5. a database unique index still needed

basics

~20 s

Use Rule::unique('users', 'username')->ignore($user), passing the authenticated or route-bound model so its own row is excluded from the check. Take the ignored id from the server, never from request input, and keep a unique index in the database.

solid answer

~40 s

A plain `unique:users,username` fails on edit because the user's own row already holds that name. `Rule::unique('users', 'username')->ignore($user)` adds "except this row" to the query: given a model it reads the key name and value, and `->ignore($id, 'user_id')` handles a custom key column. The ignored value must come from the server — `$request->user()` or a route-bound model — because the docs warn that user-controlled input in `ignore()` opens an SQL-injection hole, and a posted id would also let someone claim another user's name. `->where(...)` scopes the check (per tenant), and `->withoutTrashed()` stops soft-deleted rows from blocking a name. The rule is a pre-check, so a unique index is still what prevents two concurrent requests both succeeding.

code

php · 21 lines
php
<?php

use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

public function update(Request $request)
{
    $validated = $request->validate([
        'username' => [
            'required',
            'string',
            'alpha_dash',
            'max:30',
            Rule::unique('users', 'username')->ignore($request->user()),
        ],
    ]);

    $request->user()->update($validated);

    return back();
}

go deeper

for a junior

Know that unique fails on edit forms and that Rule::unique()->ignore() excludes the current row.

for a middle

Explain ignore() with a model or an id and custom key column, where(), withoutTrashed(), and where the id must come from.

for a senior

Pair the rule with a unique index for concurrency, handle case normalisation, and scope uniqueness per tenant.

for a principal

Decide which uniqueness guarantees live in the database versus validation, and how to present constraint violations consistently.

## The problem on an edit form The `unique` rule runs a query against a table and fails if any row already has the submitted value. On a **create** form that is exactly right. On a **profile-edit** form it is wrong: the user submits their unchanged username, the query finds their own row, and validation reports "The username has already been taken." The fix is to tell the rule which row to ignore. ## The fluent rule `Illuminate\Validation\Rule::unique($table, $column)` returns an `Illuminate\Validation\Rules\Unique` object: - `$table` — a table name, or a model class such as `User::class`. - `$column` — the column to check; when omitted, the name of the field being validated is used. - `->ignore($id, $idColumn = null)` — exclude the row whose `$idColumn` (default `id`) equals `$id`. - `->ignore($model)` — pass an Eloquent model; the rule reads `getKeyName()` and the key value for you. - `->where(fn ($query) => $query->where('team_id', $teamId))` — add conditions, for multi-tenant uniqueness. - `->withoutTrashed()` — exclude soft-deleted rows; by default they still count as taken. The object converts itself to a rule string (`unique:users,username,"42",id`), which is why it also works when concatenated into a pipe string, though the array form is clearer. ## Where the ignored id must come from The official docs carry a warning: never pass user-controlled input to `ignore()`; use a system-generated id or UUID from a model instance, or the application is open to SQL injection. There is also a plain logic hole: 1. The form includes a hidden `user_id` field and the rule uses `->ignore($request->input('user_id'))`. 2. An attacker posts the id of the account that owns the username they want. 3. The rule ignores the **victim's** row, finds no other match, and passes. 4. Unless a database index stops it, two accounts now share one username. So the id comes from `$request->user()` on a "my profile" page, or from the route-bound model on an admin "edit user" page — never from the body. ## Scenarios side by side | Page | Rule | |---|---| | Sign-up | `Rule::unique('users', 'username')` | | Edit my profile | `Rule::unique('users', 'username')->ignore($request->user())` | | Admin edits user `{user}` | `Rule::unique('users', 'username')->ignore($this->route('user'))` | | Per-team handles | `Rule::unique('members', 'handle')->where(fn ($q) => $q->where('team_id', $teamId))->ignore($member)` | | Freed names after deletion | add `->withoutTrashed()` | ## What the rule does not guarantee - **Concurrency.** Validation runs a check query, then the controller writes later. Two requests for the same new name can both pass. A **unique index** in the migration is what makes the second insert fail; treat the rule as the friendly error message and the index as the guarantee. - **Case.** Whether `Alice` and `alice` count as the same depends on the column's collation in your database, not on Laravel. Normalise in the request (for example lowercasing) if the product needs case-insensitive names. - **Wasted queries.** Laravel skips `unique` and `exists` when the field already failed an earlier rule, so `['required', 'string', 'max:30', Rule::unique(...)]` does not query for an over-long name. Order still matters: put the cheap rules first. ## A complete rule set for the edit form - `'username' => ['required', 'string', 'alpha_dash', 'max:30', Rule::unique('users', 'username')->ignore($request->user())]` - `alpha_dash` limits the characters; `max:30` matches the column size; `unique` checks the table, ignoring the current user. If the edit form is split across endpoints (for example a PATCH that may omit the username), combine this with `sometimes` so the rule only runs when the field is actually sent. ## What a strong answer covers 1. The failure mode: an unchanged value collides with the user's own row. 2. The fix: `Rule::unique(...)->ignore(...)`, with a model or an id and optional key column. 3. The trust boundary: the ignored id is server-derived, never posted. 4. The limits: a unique index for races, collation for case, `withoutTrashed()` for soft deletes. Candidates who stop at step 2 know the API; the ones who reach step 4 have shipped it.

  • Why is a hidden user_id field a dangerous source for ignore()?
    The docs warn that user-controlled input in `ignore()` can lead to SQL injection. Even without injection, an attacker can post another account's id, making the rule skip that account's row and accept a username that is already taken. Use `$request->user()` or the route-bound model.
  • Two users submit the same new username at the same moment and both pass validation. What prevents a duplicate?
    Only a unique index on `users.username`. The `unique` rule is a read before a write, so both checks can succeed; the database rejects the second insert or update, which you can catch and turn into a friendly error.
  • Soft-deleted users still block their old usernames. How do you free them?
    Add `->withoutTrashed()` to the rule (optionally with a custom column name). By default soft-deleted rows count as taken. The database's unique index has to allow the reuse as well, or the write will still fail.

saying these in an interview costs you the question

  • Pass the user id from a hidden form field into ignore()
  • The unique rule alone prevents duplicate usernames under concurrent requests
  • Rule::unique() ignores soft-deleted rows by default
  • ignore() needs the primary key value; a model instance is not accepted
  • On edit forms, just drop the unique rule