Beyond Laravel's built-in validation rules, writing a custom rule class, applying a rule only conditionally, and correctly validating a checkbox (which behaves surprisingly in HTML forms) round out the practical validation toolkit.
Creating a custom validation rule class
php artisan make:rule Uppercase
class Uppercase implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) {
$fail('The :attribute must be uppercase.');
}
}
}
$request->validate([
'coupon_code' => ['required', new Uppercase],
]);
A custom rule class is worth creating once a validation check is either reused across multiple forms, or complex enough that a single-line closure or regex pattern rule would hurt readability — $fail()'s message supports the same :attribute placeholder Laravel's built-in rule messages use.
A custom rule with its own configurable parameters
class MinWordCount implements ValidationRule
{
public function __construct(private int $minWords) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$wordCount = str_word_count($value);
if ($wordCount < $this->minWords) {
$fail("The :attribute must contain at least {$this->minWords} words.");
}
}
}
'description' => ['required', new MinWordCount(20)],
A simpler inline closure rule, for a one-off check
$request->validate([
'coupon_code' => [
'required',
function ($attribute, $value, $fail) {
if (! CouponCode::isValid($value)) {
$fail('The coupon code is not valid or has expired.');
}
},
],
]);
A closure rule is genuinely simpler for a one-off check used in exactly one place — reaching for a full rule class only pays off once the same logic needs reuse across more than one validation call.
Conditional validation: required only when another field has a specific value
$request->validate([
'shipping_method' => 'required|in:pickup,delivery',
'delivery_address' => 'required_if:shipping_method,delivery',
]);
required_if:field,value makes a field required only when another field equals a specific value — delivery_address here is only actually required when shipping_method is delivery, correctly optional for in-store pickup.
Conditional validation: applying a rule set only sometimes
$validator = Validator::make($request->all(), [
'email' => 'required|email',
]);
$validator->sometimes('phone', 'required', function ($input) {
return $input->contact_method === 'phone';
});
sometimes() applies an additional rule only when the given closure returns true — more flexible than required_if for a condition that isn't a simple equality check against one other field's value.
Validating a checkbox correctly
$request->validate([
'terms_accepted' => 'accepted',
]);
The accepted rule (rather than plain required) is specifically designed for a checkbox — it passes for "yes", "on", 1, "1", or true. This matters because an HTML checkbox that isn't checked doesn't send its field in the request data at all — plain required would fail confusingly in a way that's harder to reason about than accepted's explicit, checkbox-aware handling.
Handling the "unchecked checkbox sends nothing" behavior explicitly
Placing a hidden input with the same name immediately before the checkbox, valued 0, ensures the field is always present in the submitted data (either as 0 if unchecked, since the browser sends the last matching field, or 1 if checked) — genuinely useful when the application logic needs to explicitly know "was this ever set to false" rather than just "was it absent."
Why $fail() in a custom rule doesn't stop other rules from also running
Calling $fail() inside a custom rule records that specific failure but doesn't halt the entire request's validation — other fields (and other rules on the same field, depending on the rule set) continue being checked, which is exactly why a request can come back with several validation errors at once rather than stopping at the very first one encountered.