How to Implement Laravel Custom Validation Rules for Complex Logic

Laravel 12 recommends encapsulating complex validation logic in dedicated rule objects that implement Illuminate\Contracts\Validation\Rule or extend Illuminate\Validation\InvokableValidationRule, enabling dependency injection, reusability, and clean separation from controllers.

When building applications with the laravel/framework repository, you will eventually encounter validation scenarios that exceed the capabilities of built-in rules like required or email. Implementing Laravel custom validation rules through dedicated objects provides a structured, testable approach for handling multi-step validation, external API calls, and cross-field dependencies.

Why Use Laravel Custom Validation Rules for Complex Logic?

Relying on closure-based validation or inline controller logic quickly becomes unmaintainable. Custom rule objects solve this by offering:

  • Separation of Concerns – Validation logic lives outside controllers and Form Request classes, reducing clutter in your HTTP layer.
  • Reusability – The same rule can be attached to multiple fields across different requests without code duplication.
  • Testability – You can unit test the rule class directly without bootstrapping a full HTTP request or hitting the database.
  • Dependency Injection – Constructors can receive services (e.g., repositories, external APIs) via Laravel’s service container, as implemented in src/Illuminate/Validation/Validator.php.

Three Approaches to Creating Laravel Custom Validation Rules

The framework provides three primary patterns for implementing custom rules, each suited to different complexity levels.

Standard Rule Objects (Implements Rule Contract)

The most explicit approach requires implementing the Illuminate\Contracts\Validation\Rule interface, defined in src/Illuminate/Contracts/Validation/Rule.php. This contract requires two methods: passes($attribute, $value) and message().

Use this approach when you need clear method separation and static error messages.

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use App\Services\PhoneNumberService;

class ComplexPhoneNumber implements Rule
{
    protected $service;

    public function __construct(PhoneNumberService $service)
    {
        $this->service = $service;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed   $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        // Basic format check
        if (! preg_match('/^\+\d{1,3}\s?\d{7,14}$/', $value)) {
            return false;
        }

        // Call external service for number reputation
        $reputation = $this->service->checkReputation($value);

        // Ensure reputation score is acceptable
        return $reputation->score >= 80;
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The :attribute is not a valid phone number or has a low reputation score.';
    }
}

Usage in a Form Request or Controller:

use App\Rules\ComplexPhoneNumber;

$request->validate([
    'contact_phone' => ['required', new ComplexPhoneNumber],
]);

Because the constructor type-hints PhoneNumberService, Laravel’s container automatically injects the service when resolving the rule.

Invokable Rules (Extends InvokableValidationRule)

For developers who prefer single-method classes, Laravel offers Illuminate\Validation\InvokableValidationRule, located in src/Illuminate/Validation/InvokableValidationRule.php. This base class utilizes PHP’s __invoke magic method, making the rule callable as a function.

This approach is ideal when you want cleaner syntax and still need dependency injection, but prefer the simplicity of a single validation method.

<?php

namespace App\Rules;

use Illuminate\Validation\InvokableValidationRule;
use App\Services\ReferralService;

class ValidReferralCode extends InvokableValidationRule
{
    protected $referralService;

    public function __construct(ReferralService $referralService)
    {
        $this->referralService = $referralService;
    }

    /**
     * @param  string $attribute
     * @param  mixed  $value
     * @param  callable $fail
     */
    public function __invoke($attribute, $value, $fail)
    {
        // External API verification
        $result = $this->referralService->verify($value);

        if (! $result->valid) {
            $fail('The referral code is invalid.');
            return;
        }

        // Usage limit check
        if ($result->usageCount >= $result->maxUsage) {
            $fail('The referral code has already reached its usage limit.');
        }
    }
}

Usage:

use App\Rules\ValidReferralCode;

$request->validate([
    'referral_code' => ['nullable', new ValidReferralCode],
]);

Closure-Based Rules (For Quick Checks)

While src/Illuminate/Validation/Validator.php supports passing anonymous functions to Validator::make, this approach is not recommended for complex or reusable logic. Closures work for one-off checks but sacrifice testability and separation of concerns.

Handling Multi-Field Dependencies in Laravel Custom Validation Rules

Complex validation often requires comparing values across multiple fields (e.g., ensuring a start_date precedes an end_date). You can handle this by passing the entire data set to the rule’s constructor.

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class ChronologicalDates implements Rule
{
    protected $allData;

    public function __construct(array $data)
    {
        $this->allData = $data;
    }

    public function passes($attribute, $value)
    {
        $start = $this->allData['start_date'] ?? null;
        $end   = $this->allData['end_date'] ?? null;

        if (! $start || ! $end) {
            return false;
        }

        return strtotime($start) < strtotime($end);
    }

    public function message()
    {
        return 'The start date must be earlier than the end date.';
    }
}

Apply in a Form Request:

public function rules()
{
    return [
        'start_date' => ['required', 'date'],
        'end_date'   => ['required', 'date'],
        'start_date' => [new ChronologicalDates($this->all())],
    ];
}

Key Source Files in the Laravel Framework

Understanding the underlying implementation helps when debugging or extending validation behavior. These files define the contracts and resolution logic for Laravel custom validation rules:

Summary

  • Encapsulate complexity in dedicated rule classes rather than closures to maximize reusability and testability.
  • Choose the implementation style based on your needs: implement Illuminate\Contracts\Validation\Rule for explicit method separation, or extend Illuminate\Validation\InvokableValidationRule for a single-method callable approach.
  • Leverage dependency injection in rule constructors to access services, APIs, or repositories, as supported by the container resolution in src/Illuminate/Validation/Validator.php.
  • Handle cross-field validation by passing the full data set to your rule’s constructor when comparing multiple inputs.
  • Store rules in app/Rules (or a domain-specific directory) and reference them directly in validation arrays using the new keyword.

Frequently Asked Questions

What is the difference between Rule objects and Invokable rules in Laravel?

Rule objects implement the Illuminate\Contracts\Validation\Rule interface, requiring explicit passes() and message() methods. Invokable rules extend Illuminate\Validation\InvokableValidationRule and use a single __invoke() method that receives a $fail callback for error handling. Both support dependency injection, but invokable rules offer a more concise syntax for simple single-check logic.

Can I inject dependencies into Laravel custom validation rules?

Yes. Because Laravel resolves rule objects through its service container, you can type-hint any class in the constructor. For example, you can inject a PhoneNumberService or ReferralService directly, and Laravel will automatically instantiate and inject the dependency when the rule is created, as handled in src/Illuminate/Validation/Validator.php.

How do I validate fields that depend on other fields in Laravel?

Pass the entire request data array to your rule’s constructor. Inside the passes() or __invoke() method, access the other field values from this stored data set to perform comparisons (e.g., ensuring a start date is before an end date). This approach keeps the validation logic encapsulated while allowing cross-field awareness.

Where should I store custom validation rules in a Laravel application?

Store custom rules in the app/Rules directory, which is the default location when you run php artisan make:rule RuleName. For larger applications, organize rules into domain-specific subdirectories (e.g., app/Rules/Auth, app/Rules/Billing) to maintain a clean architecture and improve discoverability.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →