Unique field validation in Laravel

When building Laravel applications, and perhaps in any web application, validating user input is important. A common requirement is ensuring uniqueness – think email addresses, usernames, or even unique slugs. While Laravel's unique validation rule seems straightforward at first glance, it offers a surprising level of flexibility and power when you dig deeper. This article will move beyond the basic usage and explore some of its more nuanced capabilities, empowering you to build robust and secure validation logic.

Getting Specific: Table and Column Customization

The fundamental unique rule syntax is unique:table,column, e.g.

'email' => 'unique:users,email',
Databases in VS Code? Get DevDb

This checks if the provided email value already exists in the email column of the users table. In the above, the email can be omitted since it is the same as the name of the field being validated. But what if your column name doesn't match the field name? Laravel has you covered. You can explicitly specify the column:

'email_address' => 'unique:users,email', // Field is 'email_address', column is 'email'

Even better, for improved maintainability, use the Eloquent model instead of the table name directly:

'email' => 'unique:App\Models\User,email', // Uses the User model's table

This is particularly useful when you rename the underlying table.

You might also have scenarios where your validation needs to interact with a different database connection. Laravel allows you to prepend the connection name to the table:

'email' => 'unique:connection.users,email', // Uses a connection named 'connection'

This is useful when the specific user is related to a separated user database.

The "Ignore" Clause: Gracefully Handling Updates

One of the most common "gotchas" with unique validation arises during update operations. Consider a typical "Update Profile" scenario. The user already has an email address, and if they don't change it, you don't want the unique validation to fail. This is where the ignore method comes into play.

Instead of string-based rules, we'll use the Rule class instead, for a more fluent approach:

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

Validator::make($request->all(), [
    'email' => [
        'required',
        Rule::unique('users')->ignore($user->id), // Ignore the current user's ID
    ],
]);

This tells the validator: "Ensure the email is unique, except if it belongs to the user with the ID $user->id." It is important to note here that you should never pass user-provided input directly to ignore(). Always use a system-generated ID (like an auto-incrementing primary key or a UUID) to prevent potential SQL injection vulnerabilities.

Laravel is smart enough to handle Eloquent model instances directly:

Rule::unique('users')->ignore($user), // Laravel extracts the primary key

If your primary key column isn't named id, you can specify it:

Rule::unique('users')->ignore($user->user_id, 'user_id'), // Custom primary key column

And, just like before, you can still specify the column to check for uniqueness if it's different from the field name:

    Rule::unique('users', 'email_address')->ignore($user->user_id, 'user_id')

Fine-Grained Control: Adding where Clauses and soft deletes

Sometimes, you need even more control over the uniqueness check. Perhaps you want to limit the check to a specific subset of records. The where method allows you to add arbitrary conditions to the underlying query:

'email' => Rule::unique('users')->where(fn ($query) => $query->where('account_id', 1)),  // Only check within account_id = 1

This example ensures email uniqueness only within records where account_id is 1. This is incredibly useful for multi-tenant applications or scenarios where uniqueness is scoped. In a situation where you use soft deletes, the unique validation can take in consideration the deleted records. If you need to ignore them, the withoutTrashed exists:

'email' => Rule::unique('users')->withoutTrashed(),

If the soft delete uses other column name than deleted_at, it's possible to specify it:

'email' => Rule::unique('users')->withoutTrashed('was_deleted_at'),

The query changes, and it will take in consideration the provided column to check if the registry was soft deleted.

Conclusion

Laravel's unique validation rule is far more than a simple check. By understanding and utilizing the ignore, where, and connection customization options, you can craft precise and secure validation logic that adapts to a wide range of application requirements. The Rule class is more preferable over string-based rules because it offers better readability and maintainability, especially as your validation logic becomes more complex. All the rule syntax we discussed in this article is not only limited to usages in Controllers, but also in tools that support Laravel rules, such as in Livewire and Filament Forms.

Wanna chat about what you just read, or anything at all? Click here to tweet at me on 𝕏