unique

Fails when the value already exists in the given database table column.

Signature
unique:table,column

Parameters

ParameterTypeDescription
$tablestringTable name, or a fully qualified Eloquent model class whose table is used. May be prefixed with a connection name, e.g. `connection.users`.
$columnoptionalstring= the field nameColumn to search. Defaults to the name of the field under validation.

Errors

Illuminate\Database\QueryException if the table or column does not exist, or if the named connection is not configured.

The field under validation must not exist within the given database table. Every check is a real SELECT, so this rule is the point at which validation starts costing database round trips.

Updates need ignore()

On an "edit profile" screen the user already owns their address, so a plain unique:users rejects a save where the email was not even touched. Switch to the fluent form and exclude the current record: Rule::unique('users')->ignore($user->id). You may pass the whole model instead of its key, and a second argument when the primary key is not id.

You should never pass any user controlled request input into the ignore method. Pass only a system generated unique ID such as an auto-incrementing ID or UUID from an Eloquent model instance, or your application will be vulnerable to an SQL injection attack.

Soft deletes are included by default

A soft-deleted row still occupies its email address, so re-registration fails for no visible reason. Call withoutTrashed() — optionally with a custom column name — to exclude them.

Scope it in multi-tenant apps

Uniqueness is usually per-account, not global. Add ->where(fn (Builder $q) => $q->where('account_id', 1)).

Example

Example · php
<?php

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

// Creating: the address must be unused anywhere.
$create = Validator::make(['email' => '[email protected]'], [
    'email' => ['required', 'email', 'unique:users,email'],
]);

// Updating: the user's own row must not count against them.
$update = Validator::make(['email' => '[email protected]'], [
    'email' => [
        'required',
        'email',
        Rule::unique('users')->ignore($user->id),
    ],
]);

var_dump($create->passes());   // bool(false) - already registered
var_dump($update->passes());   // bool(true)  - it is his own address
print_r($create->errors()->all());

// Array
// (
//     [0] => The email has already been taken.
// )

When to use it

  • Rejecting a registration whose email address is already registered.
  • Letting a user save their profile without tripping over their own email, via ignore().
  • Enforcing per-tenant uniqueness of a project slug with a where() clause.
  • Allowing a soft-deleted account's email to be reused by calling withoutTrashed().

Frequently asked questions

How do I stop unique failing when a user edits their own record?

Use the fluent form and exclude the current row: `Rule::unique('users')->ignore($user->id)`. You can pass the model instance itself, and a second argument names the primary key column when it is not `id`. Never pass request input into `ignore()` — only a system-generated key.

Why does unique fail for an email that was deleted?

Because the rule includes soft-deleted rows by default, and a trashed user still occupies the address. Call `withoutTrashed()` on the rule, optionally passing a column name if your model uses something other than `deleted_at`.

Is the unique rule enough to prevent duplicate rows?

No. It performs a SELECT that a concurrent request can pass simultaneously, leaving a race between the check and the insert. Add a unique index in the migration and catch the resulting QueryException; treat the rule as a way to produce a good error message, not as the guarantee.

More examples

The four ways to shape the query

These four cover essentially every real requirement. Note the second argument to `ignore()` — needed when the table's primary key is not `id` — and that `withoutTrashed()` accepts a column name of its own if the model soft-deletes into something other than `deleted_at`.

Example · php
<?php

use Illuminate\Database\Query\Builder;
use Illuminate\Validation\Rule;

// Model class instead of a table name.
$a = 'unique:App\Models\User,email_address';

// A non-default connection.
$b = 'unique:reporting.users,email_address';

// A different column, plus ignoring the current record
// (and a non-standard primary key column).
$c = Rule::unique('users', 'email_address')->ignore($user->id, 'user_id');

// Scoped to a tenant, ignoring soft-deleted rows.
$d = Rule::unique('projects', 'slug')
    ->where(fn (Builder $query) => $query->where('account_id', $accountId))
    ->withoutTrashed();

print_r([$a, $b, (string) $c, (string) $d]);

A form request that serves both create and update

`ignore(null)` is a no-op, so the same rule set works for both routes without an `if`. The route model is a system-generated instance, which is exactly what the documentation says `ignore()` should receive — never `$this->input('id')`.

Example · php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class SaveUserRequest extends FormRequest
{
    public function rules(): array
    {
        $user = $this->route('user');   // null on store, a User on update

        return [
            'name'  => ['required', 'string', 'max:255'],
            'email' => [
                'required',
                'email',
                'max:255',
                Rule::unique('users')->ignore($user)->withoutTrashed(),
            ],
        ];
    }
}

// print_r($request->validated());
//
// Array
// (
//     [name] => Taylor
//     [email] => [email protected]
// )

Validation cannot replace a unique index

The `unique` rule reads and the insert writes, with a window in between. Under concurrency both requests see no existing row and both proceed. The database index is what actually enforces uniqueness; the validation rule exists to produce a friendly message in the common case.

Example · php
<?php

use Illuminate\Database\QueryException;

// The migration is the real guarantee.
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('email')->unique();
    $table->timestamps();
});

// Two concurrent requests can both pass the SELECT that
// 'unique:users' performs, then both attempt the INSERT.
try {
    $user = User::create($request->validated());

    echo 'created ' . $user->id;
} catch (QueryException $e) {
    // 23000 / 23505: the database caught what validation could not.
    echo 'duplicate email, code ' . $e->getCode();
}

Discussion

  • Be the first to comment on this lesson.