Validation internals — Illuminate/Validation/Validator.php
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:
validate()is called on the Factory, creating aValidatorinstance.- For each rule on each attribute, the Validator looks up the rule implementation.
- Built-in rules are in
\Illuminate\Validation\Concerns\ValidatesAttributes(one method per rule, e.g.,validateRequired,validateEmail,validateMin). - If validation fails, a
ValidationExceptionis thrown with theValidatorinstance. - Laravel's exception handler converts
ValidationExceptionto a422response witherrorsJSON.
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
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