0

Validation internals — Illuminate/Validation/Validator.php

Expert5 min read·lv-11-010
laravel-srcframework

Concept

Understanding how Illuminate\Validation\Validator works internally enables building custom validation rules, conditional validation, and debugging unexpected validation behavior.

The Validator class: \Illuminate\Validation\Validator handles validation state. It stores the rules, data, messages, and tracks which rules have passed/failed. It's created by the ValidatorFactory (accessed via Validator::make() or validator() helper).

Validation flow:

  1. validate() is called on the Factory, creating a Validator instance.
  2. For each rule on each attribute, the Validator looks up the rule implementation.
  3. Built-in rules are in \Illuminate\Validation\Concerns\ValidatesAttributes (one method per rule, e.g., validateRequired, validateEmail, validateMin).
  4. If validation fails, a ValidationException is thrown with the Validator instance.
  5. Laravel's exception handler converts ValidationException to a 422 response with errors JSON.

Custom rules (Rule objects): Implement Illuminate\Contracts\Validation\Rule: passes(string $attribute, mixed $value): bool and message(): string. Or use Illuminate\Contracts\Validation\InvokableRule (PHP 8 style) with __invoke($attribute, $value, $fail).

Rule::unique()->ignore($id): Excludes a record from uniqueness check (for update validation).

sometimes rule: Only validate the field if it's present in the input. 'optional_field' => 'sometimes|string|max:255'.

Validator::after() callback: Add custom post-validation logic: $validator->after(fn($v) => $v->errors()->add('field', 'Custom error')).

Code Example

php
<?php
use Illuminate\Contracts\Validation\InvokableRule;

// PHP 8.1+ style custom rule (invokable)
class ValidPhoneNumber implements InvokableRule
{
    public function __invoke(string $attribute, mixed $value, $fail): void
    {
        // Basic E.164 format check
        if (!preg_match('/^\+[1-9]\d{1,14}$/', $value)) {
            $fail("The :attribute must be a valid phone number in E.164 format (+1234567890).");
        }
    }
}

// Usage in validation
$request->validate([
    'phone' => ['required', 'string', new ValidPhoneNumber()],
]);

// Rule classes — for more complex rules
class Uppercase implements \Illuminate\Contracts\Validation\Rule
{
    public function passes(string $attribute, mixed $value): bool
    {
        return strtoupper($value) === $value;
    }
    public function message(): string
    {
        return 'The :attribute must be uppercase.';
    }
}

// Rule::unique with ignore (for update)
$validator = \Illuminate\Support\Facades\Validator::make($data, [
    'email' => [
        'required',
        'email',
        \Illuminate\Validation\Rule::unique('users', 'email')->ignore($user->id),
    ],
]);

// Conditional validation — sometimes
$validator = Validator::make($data, [
    'billing_address' => 'required_if:payment_type,card|string|max:255',
    'card_number' => 'sometimes|required|string', // only validate if present
]);

// Validator::after — add custom logic after all rules run
$validator = Validator::make($request->all(), [...]);
$validator->after(function($v) {
    if ($this->isDoubleBooked($v->getData())) {
        $v->errors()->add('time_slot', 'This time slot is already booked.');
    }
});
if ($validator->fails()) {
    throw new \Illuminate\Validation\ValidationException($validator);
}

// Accessing errors after validation
$errors = $validator->errors(); // MessageBag
$errors->get('email');          // all email errors
$errors->first('email');        // first email error
$errors->all();                 // all errors flat