The Rules Worth Knowing
required, nullable and sometimes answer three different questions, and unique and exists take their table and column as arguments.
'slug' => ['required', 'string', 'max:140', 'unique:posts,slug,17,id']A rule is a string, and its parameters follow a colon: max:120, unique:posts,slug. Give each field an array of rule strings rather than one pipe-separated string; the array form survives a rule object being added to it later.
Three different ways to be optional
| Rule | What it asks |
|---|---|
required | The value must be present, not null, not an empty string once trimmed, and not an empty array. |
nullable | Null is allowed. When the value is null, the other non-implicit rules on that field are skipped rather than failing. |
sometimes | The field's rules run only if the key is present in the data at all. |
nullable matters more than it looks, because the global ConvertEmptyStringsToNull middleware turns an empty text input into null before validation. Without it, an optional date or url field fails every time the user leaves it blank. sometimes is for PATCH-style updates, where an absent key means "leave this alone"; ['sometimes', 'required'] says "optional to send, but not allowed to be empty if you do".
Rules that ask the database
unique:posts,slug fails when a row in posts already holds that slug. The third and fourth parameters are a value to ignore and the column it lives in, which is how an update avoids colliding with its own row: unique:posts,slug,17,id. exists:tags,id is the mirror image. Either may name a model class instead of a table, and the table and key name are read from the model.
Arrays and nested keys
confirmed compares the field to {field}_confirmation; confirmed:repeat_password names a different field. For arrays, give the array one entry and its members another with *: items and items.*.sku. Messages come back under the real index, items.0.sku.
Example
<?php
// app/Http/Requests/UpdateUserRequest.php
declare(strict_types=1);
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, array<mixed>>
*/
public function rules(): array
{
/** @var User $user */
$user = $this->route('user'); // resolved by route model binding
return [
// Absent means "do not change"; present means "not empty".
'name' => ['sometimes', 'required', 'string', 'max:80'],
// Unique across users, except the row being edited.
'email' => [
'sometimes',
'required',
'email',
Rule::unique('users', 'email')->ignore($user->getKey()),
],
// An empty input arrives as null, and nullable stops the
// url rule from running on it.
'website' => ['nullable', 'url', 'max:255'],
// Needs a password_confirmation field in the same request.
'password' => ['sometimes', 'required', 'string', 'min:12', 'confirmed'],
// The array itself, then every member of it.
'role_ids' => ['sometimes', 'array', 'min:1'],
'role_ids.*' => ['integer', 'exists:roles,id'],
];
}
}When to use it
- Let a PATCH endpoint accept any subset of a record's fields.
- Keep an email column unique on update without the record colliding with itself.
- Check that every id in a posted array exists in another table.
- Accept a blank optional field from a browser form without a validation error.
More examples
The string form and the object form are the same rule
Rule::unique() is a builder that stringifies into the rule string, so both lines reach the same code. Prefer the object once there is an id to ignore, because string concatenation is where the wrong column name and the wrong argument order hide. where(), whereNot(), whereNull(), whereIn() and withoutTrashed() are only available on the object.
use Illuminate\Validation\Rule;
// Shorthand: the whole rule as one string.
'email' => ['required', 'email', 'unique:users,email,'.$user->getKey().',id'],
// Longhand: the builder object. Its __toString() produces
// unique:users,email,"17",id - the same rule, assembled for you.
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($user->getKey()),
],
// The object form can also add query constraints, which the plain
// string cannot express at all.
'email' => [
'required',
'email',
Rule::unique('users', 'email')
->ignore($user->getKey())
->whereNull('deleted_at')
->where('account_id', $user->account_id),
],A nested array and the error keys it produces
Two entries are needed: one for the array and one for its members, because a rule on lines says nothing about what is inside it. The wildcard is expanded to concrete indexes before the messages are built, which is why error keys contain 0 and 1 and why a template has to look them up by index.
// In a form request's rules(), for a body like:
// { "lines": [ { "sku": "A1", "qty": 2 }, { "sku": "", "qty": 0 } ] }
return [
'lines' => ['required', 'array', 'min:1', 'max:50'],
'lines.*.sku' => ['required', 'string', 'size:2'],
'lines.*.qty' => ['required', 'integer', 'min:1'],
// A nested key with no wildcard, for a single object.
'address.postcode' => ['required', 'string', 'max:10'],
// array with parameters restricts which keys may appear.
'meta' => ['sometimes', 'array:source,campaign'],
];
// Failures for the second line come back under the real index:
// {
// "errors": {
// "lines.1.sku": ["The lines.1.sku field is required."],
// "lines.1.qty": ["The lines.1.qty field must be at least 1."]
// }
// }What required considers empty
Useful for predicting a failure without running the validator. A field of spaces fails required, and so does an empty array, which surprises people validating a multi-select: ['required', 'array'] rejects a form where nothing was ticked, because the box arrives as an empty array.
$input = ['title' => 'Hi', 'body' => ' ', 'tags' => []];
// blank() applies the same test required does: null, a string that is
// empty after trimming, or an empty array or Countable.
$missing = collect(['title', 'body', 'tags'])
->filter(fn (string $key) => blank($input[$key] ?? null))
->values()
->all();
print_r($missing);
// Output:
// Array
// (
// [0] => body
// [1] => tags
// )
Discussion