Laravel incluye una amplia lista de reglas de validación integradas, pero hay ocasiones en las que necesitas lógica de validación específica de tu aplicación. Con las reglas personalizadas puedes definir lógica de validación reutilizable en clases o closures y usarla igual que las reglas estándar.Hay dos formas principales de definir reglas personalizadas:
Objetos de regla — muy reutilizables y fáciles de testear.
Closures — recomendados para reglas simples de un solo uso.
Para reportar varios errores en un mismo campo, llama a $fail más de una vez.
public function validate(string $attribute, mixed $value, Closure $fail): void{ if (! is_string($value)) { $fail('The :attribute must be a string.'); return; } if (strlen($value) < 8) { $fail('The :attribute must be at least 8 characters.'); } if (! preg_match('/[A-Z]/', $value)) { $fail('The :attribute must contain at least one uppercase letter.'); }}
Para reglas simples que solo se usan una vez en la aplicación, puedes definirlas mediante closures sin crear una clase.
use Illuminate\Support\Facades\Validator;use Closure;$validator = Validator::make($request->all(), [ 'title' => [ 'required', 'max:255', function (string $attribute, mixed $value, Closure $fail) { if ($value === 'foo') { $fail("The {$attribute} is invalid."); } }, ],]);
Las reglas por closure son cómodas porque se definen en línea, pero son difíciles de reutilizar y de testear de forma aislada, así que si necesitas usar la lógica en varios sitios es preferible extraerla a un objeto de regla.
Reglas implícitas (se ejecutan aunque el valor esté vacío)
Por defecto, si el campo está vacío o no existe, las reglas personalizadas no se ejecutan. Si quieres que la regla se ejecute también con valores vacíos, genera la clase con la opción --implicit.
php artisan make:rule Uppercase --implicit
La clase generada implementa la interfaz ImplicitRule. Esta interfaz no añade métodos; simplemente actúa como señal para Laravel.
<?phpnamespace App\Rules;use Closure;use Illuminate\Contracts\Validation\ImplicitRule;use Illuminate\Contracts\Validation\ValidationRule;class RequiredIfJapanese implements ValidationRule, ImplicitRule{ public function validate(string $attribute, mixed $value, Closure $fail): void { if (empty($value)) { $fail('The :attribute is required.'); return; } if (! preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー]+$/u', $value)) { $fail('The :attribute must contain only Japanese characters.'); } }}
ImplicitRule solo indica a Laravel que «el atributo es obligatorio». Que la validación realmente falle cuando el valor esté vacío depende de lo que hagas dentro del método validate.
DataAwareRule — acceso a todos los datos del formulario
Si necesitas validar en función del valor de otros campos, implementa la interfaz DataAwareRule. El método setData se invoca automáticamente antes de empezar la validación.
<?phpnamespace App\Rules;use Closure;use Illuminate\Contracts\Validation\DataAwareRule;use Illuminate\Contracts\Validation\ValidationRule;use Illuminate\Support\Facades\DB;class UniqueForTenant implements DataAwareRule, ValidationRule{ /** * Todos los datos objeto de validación * * @var array<string, mixed> */ protected array $data = []; public function __construct( protected string $table, protected string $column = 'value', ) {} /** * Establece los datos de la validación * * @param array<string, mixed> $data */ public function setData(array $data): static { $this->data = $data; return $this; } public function validate(string $attribute, mixed $value, Closure $fail): void { $tenantId = $this->data['tenant_id'] ?? null; $exists = DB::table($this->table) ->where('tenant_id', $tenantId) ->where($this->column, $value) ->exists(); if ($exists) { $fail('The :attribute has already been taken for this tenant.'); } }}
ValidatorAwareRule — acceso a la instancia del validator
Para acceder a toda la información del validator (reglas que han fallado, mensajes personalizados, etc.), implementa la interfaz ValidatorAwareRule.
<?phpnamespace App\Rules;use Closure;use Illuminate\Contracts\Validation\ValidationRule;use Illuminate\Contracts\Validation\ValidatorAwareRule;use Illuminate\Validation\Validator;class ConditionalFormat implements ValidationRule, ValidatorAwareRule{ protected Validator $validator; public function setValidator(Validator $validator): static { $this->validator = $validator; return $this; } public function validate(string $attribute, mixed $value, Closure $fail): void { // Salta si otro campo ya ha fallado la validación if ($this->validator->errors()->has('type')) { return; } $type = $this->validator->getData()['type'] ?? null; if ($type === 'phone' && ! preg_match('/^\+?[0-9\-\s]+$/', $value)) { $fail('The :attribute must be a valid phone number.'); } if ($type === 'email' && ! filter_var($value, FILTER_VALIDATE_EMAIL)) { $fail('The :attribute must be a valid email address.'); } }}
Regla para validar el conjunto de caracteres (por ejemplo, japonés).
php artisan make:rule JapaneseOnly
<?phpnamespace App\Rules;use Closure;use Illuminate\Contracts\Validation\ValidationRule;class JapaneseOnly implements ValidationRule{ public function validate(string $attribute, mixed $value, Closure $fail): void { // Solo permite hiragana, katakana, kanji y el signo de alargamiento if (! preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー\s]+$/u', $value)) { $fail('El campo :attribute debe estar en japonés (hiragana, katakana o kanji).'); } }}
$request->validate([ 'name_kana' => ['required', 'string', new JapaneseOnly],]);
Regla para validar el formato de números de teléfono de Japón.
php artisan make:rule JapanesePhone
<?phpnamespace App\Rules;use Closure;use Illuminate\Contracts\Validation\ValidationRule;class JapanesePhone implements ValidationRule{ public function validate(string $attribute, mixed $value, Closure $fail): void { // Admite con o sin guiones y también el formato internacional $normalized = preg_replace('/[\s\-\(\)]/', '', $value); $patterns = [ '/^0\d{9,10}$/', // Número fijo o móvil habitual '/^\+81\d{9,10}$/', // Formato internacional ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $normalized)) { return; } } $fail('El campo :attribute debe tener un formato de teléfono válido.'); }}
Con Validator::extend() puedes usar la regla personalizada en formato de cadena ('nombre_de_la_regla'). Regístrala en el método boot() de AppServiceProvider.
<?phpnamespace App\Providers;use Illuminate\Support\Facades\Validator;use Illuminate\Support\ServiceProvider;use Illuminate\Validation\Validator as ValidatorInstance;class AppServiceProvider extends ServiceProvider{ public function boot(): void { Validator::extend('japanese_only', function (string $attribute, mixed $value, array $parameters, ValidatorInstance $validator): bool { return preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー\s]+$/u', $value) === 1; }); Validator::replacer('japanese_only', function (string $message, string $attribute): string { return str_replace(':attribute', $attribute, 'El campo :attribute debe estar en japonés.'); }); }}
Las reglas registradas con Validator::extend() pueden usarse como una cadena.
Validator::extend() es un mecanismo de registro más antiguo que los objetos de regla. Para código nuevo, se recomienda usar objetos que implementen la interfaz ValidationRule.
Veamos cómo Illuminate\Validation\Validator invoca las reglas personalizadas.Dentro del validator, validateAttribute() procesa cada campo. Si la regla implementa la interfaz ValidationRule, se llama al método validateUsingCustomRule().
// Versión simplificada de Illuminate\Validation\Validator::validateUsingCustomRule()protected function validateUsingCustomRule($attribute, $value, $rule){ // Si es DataAwareRule, inyecta todos los datos if ($rule instanceof DataAwareRule) { $rule->setData($this->getData()); } // Si es ValidatorAwareRule, inyecta el propio validator if ($rule instanceof ValidatorAwareRule) { $rule->setValidator($this); } // Llama a validate() $rule->validate($attribute, $value, function ($message, $translate = false) use ($attribute, $rule) { // El closure $fail — añade el mensaje de error $this->errors()->add($attribute, $this->makeReplacements( $message, $attribute, get_class($rule), [], // sustituciones adicionales (por ejemplo, ['min' => '8']) )); });}
El tratamiento de ImplicitRule se decide con isImplicit(), que hace que la regla se ejecute incluso cuando el valor está vacío.
// Decide si se ejecuta la regla para un valor vacíoprotected function isImplicit($rule): bool{ return $rule instanceof ImplicitRule || in_array($rule, $this->implicitRules);}
Puedes implementar DataAwareRule y ValidatorAwareRule a la vez. Si una clase implementa ambas interfaces, se ejecutan las dos inyecciones.