Laravel Validation Unique Rule With Exceptions
Laravel Validation supports rules forcing unique values - but exceptions are usually needed.
For example if you have articles on a site where each has a slug that has to be unique - when you update the article it has to possible to save the article using the slug it already has.
https://laravel.com/docs/10.x/validation#rule-unique
Request Rules
If we have a Form Request with rules() like
public function rules(): array
{
return [
'slug' => [
'required',
'unique:articles',
],
];
}
We need to add an ignore for the current article id.
But note the warning
You should never pass any user controlled request input into the ignore method. Instead, you should only pass a system generated unique ID such as an auto-incrementing ID or UUID from an Eloquent model instance. Otherwise, your application will be vulnerable to an SQL injection attack.
What I couldn’t figure out at first is how do I get the article ID here - without taking it from user input ?
Access Route Parameters
In requests you can access the route via $this->route()
and route parameters via $this->route('paramName')
See Illuminate/Http/Request.php#L634
public function route($param = null, $default = null)
{
$route = call_user_func($this->getRouteResolver());
if (is_null($route) || is_null($param)) {
return $route;
}
return $route->parameter($param, $default);
}
Rules with Parameters
Using this login in the earlier example to add an ignore for the current article.
public function rules(): array
{
return [
'slug' => [
'required',
Rule::unique('articles')->ignore($this->route('article')->id),
]
];
}
Adding a Where Clause
In my case I have multiple site running from one Laravel system - I want all articles to have a unique slug in each site.
public function rules(): array
{
return [
'slug' => [
'required',
Rule::unique('articles')
->ignore($this->route('article')->id)
->where(fn (Builder $query) => $query->where('site_id', $this->route('site')->id)),
]
];
}
Optional Parameters
I want to use the same validation logic for new articles and updated articles
When creating a new article there is no article ID.
Slugs always have to be unique per site, if this is an update we ignore the slug of the current article.
public function rules(): array
{
$unique = Rule::unique('articles')
->where(fn (Builder $query) => $query->where('site_id', $this->route('site')->id));
if ($this->route('article')) {
$unique->ignore($this->route('article')->id);
})
return [
'slug' => [
'required',
$unique,
]
];
}
Summary
By accessing named route parameters we benefit from previous data sanitisation of user input and are using the ID from the database.