Tangible Bytes

A Web Developer’s Blog

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.