In Laravel, how do you write a custom validation rule class, and when does it run for a missing or empty field?
answer
- php artisan make:rule, app/Rules
- ValidationRule::validate($attribute, $value, $fail)
- call $fail('...') instead of returning false
- skipped when absent or '' unless implicit
- DataAwareRule, ValidatorAwareRule for context
basics
~20 sRun php artisan make:rule to get a class implementing ValidationRule with validate(string $attribute, mixed $value, Closure $fail); call $fail() with a message to reject. Like ordinary rules, it is skipped for absent or empty-string fields unless it sets public $implicit = true.
solid answer
~40 s`php artisan make:rule NotReservedUsername` creates `app/Rules/NotReservedUsername.php` implementing `Illuminate\Contracts\Validation\ValidationRule`. Its single method, `validate(string $attribute, mixed $value, Closure $fail): void`, calls `$fail('The :attribute is reserved.')` to add an error; the closure returns a `PotentiallyTranslatedString`, so `->translate()` looks the message up in language files. You use it as an array element: `['required', new NotReservedUsername($reserved)]`, and the constructor can take configuration. Like other non-implicit rules, it does not run when the key is absent or an empty string; `make:rule --implicit` generates `public $implicit = true` so it always runs. Implement `DataAwareRule` to read other fields, or `ValidatorAwareRule` for the validator. The older `Rule` contract with `passes()`/`message()` is deprecated in favour of `ValidationRule`.
code
php · 19 lines<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class NotReservedUsername implements ValidationRule
{
/** @param array<int, string> $reserved */
public function __construct(private array $reserved) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (in_array(strtolower((string) $value), $this->reserved, true)) {
$fail('validation.reserved_username')->translate(['value' => $value]);
}
}
}go deeper
Know make:rule, the validate() method and that calling $fail() is how the rule rejects a value.
Explain when the rule is skipped, how implicit changes that, and how DataAwareRule exposes other fields.
Choose between closures, Rule::when and rule classes, keep rules testable, and replace deprecated passes()/message() rules.
Curate a shared library of domain rules so validation vocabulary stays consistent across teams and services.
## When a custom rule is worth it Built-in rules cover formats, sizes, database checks and conditions. A custom rule earns its place when the check is **reusable** and **not expressible** with those, for example "the username must not be a reserved word like admin, support or the brand name", used on sign-up, profile edit and the admin panel. ## Generating and writing one ```bash php artisan make:rule NotReservedUsername ``` This writes `app/Rules/NotReservedUsername.php`: - The class implements `Illuminate\Contracts\Validation\ValidationRule`. - It has one method: `validate(string $attribute, mixed $value, Closure $fail): void`. - To reject the value, call `$fail('The :attribute is reserved.')`. Returning nothing means it passed. - `$fail()` returns a `PotentiallyTranslatedString`; chaining `->translate()` treats the message as a translation key, with optional replacements. - The constructor can take configuration (a list of reserved names, a limit), making the rule reusable with different settings. Use it only in the **array** form of a field's rules: `'username' => ['required', 'string', new NotReservedUsername(config('profile.reserved'))]`. ## When the rule runs Custom rules follow the same rule as built-in ordinary rules: 1. Key **absent** → the rule is skipped. 2. Key present with an **empty string** → skipped. 3. Key present with **null** → the rule runs, unless the field also has `nullable`. 4. Any other value → the rule runs. To make the rule run even when the field is absent or empty — for "at least one of these must be set" logic — declare it **implicit**: `php artisan make:rule NotReservedUsername --implicit` adds `public $implicit = true;`. An implicit rule behaves like `required` in this respect, so it must itself handle `null` and empty input. ## Reaching the rest of the input `validate()` receives only one attribute and its value. Two optional interfaces give it more: | Interface | Method Laravel calls | Use it for | |---|---|---| | `DataAwareRule` | `setData(array $data)` | comparing against another field | | `ValidatorAwareRule` | `setValidator(Validator $validator)` | reading custom attribute names or other validator state | ## Closures, Rule::when, or a class? | Option | Good for | |---|---| | Inline closure `function (string $attribute, mixed $value, Closure $fail) {...}` | a one-off check used in one place | | `Rule::when` / `Rule::requiredIf` | choosing built-in rules based on a condition | | Rule class | a reusable check, configurable, testable in isolation | | Built-in fluent objects (`Password`, `File`, `Rule::enum`) | before writing your own, check whether one already exists | ## Legacy contracts Older tutorials implement `Illuminate\Contracts\Validation\Rule` with `passes($attribute, $value)` and `message()`, or `InvokableRule` with `__invoke()`. Both are marked `@deprecated` in favour of `ValidationRule`. They still work, but new code should use `validate()` and `$fail`. ## Testing the rule Because the rule is a plain class, a unit test can call `validate()` with a closure that records the message, or build a `Validator::make()` with the rule and assert `fails()`. Keep database access out of the rule when possible, injecting the data it needs through the constructor, so tests stay fast. ## A note on the profile scenario Reserved names complement, not replace, the uniqueness check: `['required', 'string', 'alpha_dash', 'max:30', new NotReservedUsername($reserved), Rule::unique('users', 'username')->ignore($user)]`. Placing the custom rule before `unique` keeps the error meaningful; `unique` is skipped anyway once an earlier rule has failed. ## Checklist before shipping a rule class - Is there already a built-in rule or fluent object that does this? - Does the rule need to run for missing or empty input? If so, make it implicit and handle `null` inside. - Does it need other fields? Implement `DataAwareRule` rather than reaching for the global request. - Is the message translatable (`->translate()`) and does it use `:attribute` so custom attribute names apply? - Is its configuration passed through the constructor, so tests and different endpoints can vary it?
- Why does a custom rule not fire when the user leaves the field out?The validator only runs non-implicit rules for keys that are present and not an empty string. A missing key skips every non-implicit rule, custom ones included. Add `required`, or make the rule implicit with `public $implicit = true` (`make:rule --implicit`) and handle empty values inside it.
- How can a rule class compare its field with another input field?Implement `DataAwareRule`. The validator calls `setData($data)` with all the data under validation before running the rule, so `validate()` can read the other field from the stored array.
saying these in an interview costs you the question
- validate() should return false to fail the value
- Custom rules always run, even when the field is missing
- The Rule contract with passes() and message() is the current recommended interface
- A custom ValidationRule object can be appended to a pipe-delimited string
- A rule class cannot see other fields in the request