0

Custom validation rules — Rule objects and closures

Intermediate5 min read·lv-11-006
interview

Concept

When no built-in rule covers your validation requirement, Laravel provides two mechanisms for custom rules: closure-based inline rules and dedicated Rule object classes. Both integrate cleanly with the Validator's rule resolution pipeline.

A closure rule is the quickest option. Passed directly in the rules array as a callable, it receives three arguments: $attribute (the field name), $value (the current value), and $fail (a callable you invoke with an error message string if validation fails). The Validator wraps the closure in an Illuminate\Validation\Rules\ClosureValidationRule instance internally.

The Rule object approach is better for reusable, testable, and configurable rules. You generate the stub with php artisan make:rule StrongPassword. The resulting class in app/Rules/ must implement Illuminate\Contracts\Validation\Rule (the older interface with passes() + message()) or the newer implicit variant Illuminate\Contracts\Validation\InvokableRule, which uses a single __invoke($attribute, $value, $fail) signature — the same as a closure but wrapped in a class.

Implicit rules are a special category: they run even when the field is absent or null. Non-implicit rules are skipped for missing/null fields (the nullable modifier handles that case). To make a Rule object implicit, implement Illuminate\Contracts\Validation\ImplicitRule as a marker interface alongside Rule. Use implicit rules for rules that must detect absence itself, such as a rule ensuring exactly one of several mutually exclusive fields was provided.

The $fail callable in both the closure and __invoke syntax accepts either a plain string or a translation key. Calling $fail multiple times adds multiple errors for the same field — useful when a single custom rule checks several conditions.

For database-aware custom rules (checking external services, querying related tables in complex ways), inject dependencies via the Rule class constructor. Since Rule classes are resolved through the service container if you use app()->make(), or simply instantiated with new, constructor injection works for anything you can new directly.

Code Example

php
<?php

// --- Closure rule (inline, quick) ---
use Illuminate\Validation\Validator;

$request->validate([
    'username' => [
        'required',
        'string',
        function (string $attribute, mixed $value, \Closure $fail): void {
            if (str_contains(strtolower($value), 'admin')) {
                $fail("The :attribute must not contain the word 'admin'.");
            }
        },
    ],
]);

// --- Invokable Rule class (reusable, preferred) ---
// app/Rules/StrongPassword.php

namespace App\Rules;

use Illuminate\Contracts\Validation\InvokableRule;

class StrongPassword implements InvokableRule
{
    public function __invoke(string $attribute, mixed $value, \Closure $fail): void
    {
        if (strlen($value) < 12) {
            $fail('The :attribute must be at least 12 characters.');
        }

        if (!preg_match('/[A-Z]/', $value)) {
            $fail('The :attribute must contain at least one uppercase letter.');
        }

        if (!preg_match('/[0-9]/', $value)) {
            $fail('The :attribute must contain at least one number.');
        }

        if (!preg_match('/[\W_]/', $value)) {
            $fail('The :attribute must contain at least one special character.');
        }
    }
}

// --- Parameterised Rule class ---
// app/Rules/MaxWordsRule.php

namespace App\Rules;

use Illuminate\Contracts\Validation\InvokableRule;

class MaxWords implements InvokableRule
{
    public function __construct(private readonly int $limit) {}

    public function __invoke(string $attribute, mixed $value, \Closure $fail): void
    {
        $count = str_word_count((string) $value);
        if ($count > $this->limit) {
            $fail("The :attribute must not exceed {$this->limit} words (got {$count}).");
        }
    }
}

// --- Using Rule objects in a controller or FormRequest ---
use App\Rules\StrongPassword;
use App\Rules\MaxWords;

$request->validate([
    'password' => ['required', 'confirmed', new StrongPassword()],
    'bio'      => ['nullable', 'string', new MaxWords(200)],
]);

Interview Q&A

Q: What is the difference between a standard Rule object and an implicit rule, and when do you need an implicit rule?

Standard Rule objects implement Illuminate\Contracts\Validation\Rule. The Validator skips them if the field is absent or null — just like built-in rules — unless nullable or required are also present. An implicit rule implements the additional marker interface Illuminate\Contracts\Validation\ImplicitRule, which signals the Validator to run the rule even when the field is absent. You need an implicit rule when the rule itself is responsible for detecting and reporting absence — for example a mutual-exclusivity rule that checks a whole group of fields and decides which one should have been present.


Q: How do you make a custom Rule class configurable and injectable?

Custom Rule classes are plain PHP objects — they have constructors. Pass configuration at construction time: new MaxWords(200). For dependencies that require the service container (repositories, HTTP clients), either inject them via the constructor and instantiate with app(MaxWords::class), or use a static factory method. Since Validator::make() doesn't resolve rules from the container automatically, the common pattern is to new the rule with manually provided dependencies, or bind a factory into the container that returns a configured instance. Avoid making rules depend on the request object directly; pass the data as constructor arguments to keep the rule testable in isolation.


Q: Can a custom rule add multiple error messages for a single field, and how?

Yes. The $fail callable can be called multiple times in both closure rules and InvokableRule::__invoke(). Each call appends an additional error to the field's MessageBag entry. This is useful when a single rule checks several independent conditions — for example, a password strength rule that reports all failing requirements simultaneously rather than stopping at the first failure. The user sees a complete list of what needs to be fixed rather than discovering failures one by one on successive submissions.