Reglas de validación | Laravel 13.x en español - Referencia con diferencias clave
En esta página 15
Laravel trae decenas de reglas de validación integradas y casi la mitad de los reportes “esto no valida” se deben a confundir dos reglas que parecen sinónimas. nullable y sometimes, present y required, boolean y la cadena "true", bail y stopOnFirstFailure - cada par tiene una distinción que el manual menciona en un párrafo intermedio y que basta con saltarse para que el formulario falle en producción. Esta referencia agrupa las reglas por función, marca las diferencias y muestra el código mínimo para cada caso.
Para los fundamentos de cómo se invoca el validador (validate() en el request, FormRequest, Validator::make()), la guía rápida es el punto de partida.
Cómo se declaran las reglas
Laravel admite tres sintaxis para el array de reglas. La diferencia no es estética: algunas combinaciones obligan a un formato concreto.
Cadena con pipe, la forma más compacta:
$request->validate([
'titulo' => 'required|string|max:200',
'edad' => 'required|integer|min:18',
]);
Array de strings y objetos Rule, necesario cuando una regla contiene comas o se usa un builder:
use Illuminate\Validation\Rule;
$request->validate([
'email' => ['required', 'email:rfc,dns', Rule::unique('usuarios')->ignore($usuario->id)],
'pais' => ['required', Rule::in(['mx', 'es', 'ar', 'co'])],
]);
Fluent builder vía la clase Rule, útil para reglas compuestas:
use Illuminate\Validation\Rule;
$request->validate([
'codigo' => [
'required',
Rule::string()->min(4)->max(20)->alphaDash(ascii: true),
],
'inicio' => ['required', Rule::date()->afterToday()],
]);
Pipe para reglas simples, array obligatorio cuando aparece Rule::* o cuando un patrón regex contiene |, fluent builder cuando hay condicionales o muchas restricciones encadenadas.
Presencia y “vacío”: la familia donde más se confunde
Estas reglas controlan si la clave debe existir, si puede estar vacía, y qué cuenta como vacío. La distinción entre ellas es la fuente número uno de fallos sutiles.
Antes de nada: para Laravel un valor es vacío si es null, cadena "", array vacío, Countable vacío, o un archivo subido sin path. Esta definición se repite en required, filled, prohibited y compañía.
required
El campo tiene que estar presente y no ser vacío. La regla más usada y la base mental para las demás.
$request->validate([
'titulo' => 'required|string|max:200',
'cuerpo' => 'required|string|min:10',
'etiquetas' => 'required|array|min:1',
]);
filled
Si el campo aparece, no puede estar vacío; si no aparece, pasa sin más. Útil en endpoints donde el cliente puede omitir un campo opcional, pero si lo envía debe darle un valor real.
$request->validate([
'apodo' => 'filled|string|max:60',
]);
// { } -> ok (clave ausente)
// { "apodo": "dev42" } -> ok
// { "apodo": "" } -> falla
present vs required
present exige que la clave exista en los datos, pero acepta cualquier valor incluyendo null y cadena vacía. required exige clave y valor no vacío.
$request->validate([
'comentario' => 'present|nullable|string|max:500',
]);
// { "comentario": null } -> ok
// { "comentario": "" } -> ok
// { } -> falla (clave no existe)
present casi nunca aparece solo: lo natural es present|nullable|<tipo> cuando el frontend siempre envía la clave (aunque sea como null) y la lógica del controlador necesita ese contrato. Variantes condicionales: present_if, present_unless, present_with, present_with_all.
nullable
Permite que el valor sea null. Sin esta regla, null falla cualquier validación de tipo (string, integer, date).
$request->validate([
'segundo_nombre' => 'nullable|string|max:60',
'eliminado_en' => 'nullable|date',
]);
nullable no convierte el campo en opcional ni anula a required: las dos juntas son contradictorias porque required también rechaza null (su definición de “vacío” incluye null). Si el campo siempre llega pero su valor puede ser null, lo correcto es present|nullable|<tipo>. Si el campo puede no aparecer en absoluto, sometimes|nullable|<tipo>.
La combinación nullable string aparece a menudo en bases que aceptan campos textuales opcionales: si llega null, no se evalúa string ni el resto de reglas de formato; si llega un texto, sí.
sometimes (regla): validar solo si el campo está presente
sometimes como regla (no método del validador) indica que las reglas siguientes solo se aplican si la clave existe en los datos. Si no aparece, toda la cadena se salta.
$validator = Validator::make($datos, [
'telefono' => 'sometimes|required|string|min:10',
]);
Si telefono no está en $datos, ninguna regla se ejecuta y el validador pasa. Si la clave aparece, required entra y una cadena vacía falla.
sometimes vs nullable (o nullable vs sometimes)
Las dos parecen “relajar” la validación, pero trabajan a niveles distintos:
sometimesactúa sobre la presencia de la clave. Si el campo no existe en el request, ninguna regla se ejecuta.nullableactúa sobre el valor. La clave existe (o el campo es required), peronullse acepta sin pasar por las reglas de tipo.
Dicho al revés: nullable acepta null cuando el campo llega; sometimes salta la cadena cuando el campo no llega.
// PATCH: el cliente envía solo lo que cambió. La clave puede no venir.
'bio' => 'sometimes|nullable|string|max:1000',
// POST: la clave siempre llega; su valor puede ser null (un empleado sin supervisor).
'supervisor_id' => 'present|nullable|integer|exists:usuarios,id',
sometimes vs required
Una pregunta frecuente: ¿sometimes reemplaza a required? No. Son ortogonales. sometimes decide si la cadena se ejecuta; required decide qué pasa una vez que la cadena se ejecuta. La combinación sometimes|required significa “si el campo aparece, debe tener valor no vacío”. Es lo que se necesita en PATCH para no aceptar "" cuando el cliente decide enviar el campo.
Campos opcionales en la práctica
No existe una regla optional. Para un campo totalmente opcional la combinación canónica es sometimes|nullable:
$request->validate([
'web' => 'sometimes|nullable|url',
'notas' => 'sometimes|nullable|string|max:2000',
]);
Los campos no mencionados en las reglas no se validan ni aparecen en validated(). Declarar sometimes|nullable|<tipo> permite validar el campo cuando llega, pero no lo añade al resultado si el cliente no lo envió: ningún combo de reglas materializa una clave ausente. Si necesitas la clave siempre presente en validated(), hay dos opciones: usar present|nullable|<tipo> (exige al cliente mandarla, aunque sea como null), o inyectar un valor por defecto en prepareForValidation() antes de la validación.
missing y su familia
Contrario a present: la clave no debe existir. El caso típico es una API versionada donde un parámetro fue retirado y se quiere rechazar clientes que aún lo envían:
$request->validate([
'campo_legacy' => 'missing',
]);
Variantes condicionales: missing_if:otro,valor, missing_unless:otro,valor, missing_with:a,b, missing_with_all:a,b. Útiles cuando un campo debe desaparecer al activarse un modo:
$request->validate([
'tipo' => 'required|in:invitado,registrado',
'password' => 'required_if:tipo,registrado',
'password_token' => 'missing_if:tipo,registrado',
]);
prohibited y compañía
prohibited exige que el campo esté ausente o vacío. La diferencia con missing: missing falla si la clave aparece aunque el valor sea null; prohibited acepta null, "", array vacío o archivo sin path como “vacío”.
$request->validate([
'rol_admin' => 'prohibited',
]);
La forma más usada es prohibited_if, que bloquea el campo según el valor de otro:
$request->validate([
'es_empresa' => 'required|boolean',
'razon_social' => 'required_if:es_empresa,true|prohibited_if:es_empresa,false|string|max:200',
'numero_personal' => 'prohibited_if:es_empresa,true|string|max:20',
]);
prohibits invierte la perspectiva: si el campo actual tiene valor, los listados quedan prohibidos:
$request->validate([
// si se envía mensaje_personal, plantilla_id queda prohibido
'mensaje_personal' => 'sometimes|string|max:500|prohibits:plantilla_id',
'plantilla_id' => 'required_without:mensaje_personal|integer|exists:plantillas,id',
]);
Nota sobre prohibits con booleanos: se dispara cuando el campo origen no está vacío según la definición estricta (vacío = null, "", array vacío, archivo sin path). Un booleano false no entra en esa lista, así que 'flag' => 'sometimes|boolean|prohibits:otro' dispara la prohibición tanto con flag: true como con flag: false, lo que rara vez es lo que se busca. Para banderas usa prohibited_if:flag,true.
Para checkboxes existen prohibited_if_accepted y prohibited_if_declined, que no necesitan especificar el valor:
$request->validate([
'usar_direccion_default' => 'required|boolean',
'direccion_custom' => 'prohibited_if_accepted:usar_direccion_default|string|max:400',
]);
Para condiciones que requieren código (consultar BD, comparar con usuario autenticado), Rule::prohibitedIf() acepta bool o closure:
$request->validate([
'codigo_descuento' => Rule::prohibitedIf(fn () => $pedido->estaPagado()),
]);
bail: detener el campo en el primer error
Por defecto Laravel evalúa todas las reglas de cada campo y acumula errores. bail interrumpe esa secuencia en el primer fallo del campo:
$request->validate([
'email' => 'bail|required|email:rfc|unique:usuarios',
'password' => 'required|min:8',
]);
Si email falla en required, ni email:rfc ni unique:usuarios se ejecutan. password sigue su curso porque bail solo afecta al campo donde se declara.
El uso principal es no acumular varios mensajes de error para el mismo campo cuando ya falló una regla previa: si email no tiene formato (email:rfc falla), mostrar además el mensaje de unique solo añade ruido. Para reglas costosas (regex pesado, reglas personalizadas con I/O, validaciones contra servicios externos) bail ahorra tiempo de ejecución. Por convención se coloca al inicio del pipe para que sea visible.
Para detener toda la validación tras el primer error de cualquier campo, el método stopOnFirstFailure() (no es una regla) actúa sobre el validador completo:
$validator = Validator::make($datos, $reglas);
if ($validator->stopOnFirstFailure()->fails()) {
// único error: el primero de la primera regla que haya fallado
}
O su atributo equivalente para Form Requests:
use Illuminate\Foundation\Http\Attributes\StopOnFirstFailure;
#[StopOnFirstFailure]
class GuardarPedidoRequest extends FormRequest { /* ... */ }
Cuándo evitar bail: si quieres mostrar al usuario varios problemas del mismo campo a la vez (por ejemplo “el email no tiene formato y además ya existe”), bail esconde el segundo mensaje. Reservarlo para cadenas largas donde el segundo error sería redundante o para reglas que cuesta evaluar.
Restringir valores permitidos
in y not_in (validación in lista)
in verifica que el valor pertenezca a una lista cerrada. Es la herramienta para estados, prioridades, roles:
$request->validate([
'estado' => 'required|in:borrador,publicado,archivado',
'prioridad' => ['required', Rule::in([1, 2, 3, 4, 5])],
]);
in con la sintaxis de strings compara con coincidencia laxa ("1" == 1 devuelve true), así que declarar integer o string antes hace explícito el tipo esperado y previene sorpresas si el origen mezcla tipos.
not_in rechaza si el valor está en la lista:
$request->validate([
'nombre_usuario' => ['required', 'string', Rule::notIn(['admin', 'root', 'sistema'])],
]);
enum: validación con PHP Enums
Para enumeraciones respaldadas existe la regla Rule::enum(). Verifica que el valor enviado coincida con alguno de los cases:
enum EstadoPedido: string
{
case Pendiente = 'pendiente';
case Pagado = 'pagado';
case Enviado = 'enviado';
case Cancelado = 'cancelado';
}
$request->validate([
'estado' => ['required', Rule::enum(EstadoPedido::class)],
]);
Rule::enum() está pensada para backed enums (con tipo string o int). Aplicada a un enum puro la validación no resuelve correctamente: la regla asume internamente tryFrom(), que solo existe en backed enums. Si los valores válidos no vienen de un enum sino de configuración o BD, Rule::in() es la alternativa directa.
Los métodos only() y except() restringen qué valores del enum son válidos para una llamada concreta:
// el cliente solo puede mover entre estos dos
Rule::enum(EstadoPedido::class)->only([
EstadoPedido::Pendiente,
EstadoPedido::Cancelado,
]);
// admite todos menos cancelado
Rule::enum(EstadoPedido::class)->except([EstadoPedido::Cancelado]);
Lógica condicional con when():
Rule::enum(EstadoPedido::class)->when(
$usuario->esGerente(),
fn ($regla) => $regla->only([EstadoPedido::Enviado, EstadoPedido::Cancelado]),
fn ($regla) => $regla->except([EstadoPedido::Enviado]),
);
anyOf: OR entre conjuntos de reglas
Rule::anyOf() define alternativas; el valor es válido si pasa al menos uno de los sets:
$request->validate([
'contacto' => [
'required',
Rule::anyOf([
['email'],
['string', 'regex:/^\+\d{10,15}$/'],
]),
],
]);
contacto puede ser email o teléfono internacional. Otro patrón clásico: aceptar UUID o ID numérico:
$request->validate([
'referencia' => [
'required',
Rule::anyOf([
['uuid'],
['integer', 'min:1'],
]),
],
]);
Sin anyOf habría que escribir una regla personalizada o separar el input en dos campos.
boolean: lo que acepta y lo que no
La regla boolean admite true, false, 1, 0, "1" y "0". Lo que no acepta sin más es "true" ni "false" como strings. De ahí el clásico reporte “booleano no funciona” cuando el frontend serializa un checkbox como "true" desde un JSON sin parsear o desde un <select>.
El fix habitual es normalizar el valor antes de validar, en prepareForValidation() del Form Request:
protected function prepareForValidation(): void
{
if (is_string($this->activo)) {
$this->merge([
'activo' => filter_var($this->activo, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE),
]);
}
}
Existe boolean:strict, que solo acepta los valores PHP nativos true y false:
$request->validate([
'activo' => 'boolean', // acepta 1/0/"1"/"0"/true/false
'notificaciones' => 'boolean:strict', // solo true/false PHP
]);
Un booleano con nullable permite tres estados (true, false, null), útil para preferencias no definidas:
$request->validate([
'marketing_emails' => 'nullable|boolean',
]);
accepted y declined
accepted se valida con "yes", "on", 1, "1", true, "true". Pensado para checkboxes obligatorios (términos, política):
$request->validate([
'terminos' => 'accepted',
'politica_privacidad' => 'accepted',
]);
declined es lo contrario ("no", "off", 0, "0", false, "false"). Ambos tienen variantes condicionales: accepted_if:campo,valor y declined_if:campo,valor.
Precondición importante: accepted es una regla implícita. Si la clave no llega en el request, la validación falla con el mismo mensaje “debe ser aceptado”. Y en HTML los checkboxes desmarcados no se envían: un campo opcional declarado con accepted siempre fallará cuando el usuario no marque la casilla. Para checkboxes que pueden no aparecer, usar sometimes|accepted (solo valida si llega) o boolean con normalización en prepareForValidation().
Diferencia con boolean: accepted no valida un booleano genérico, sino la presencia de un valor “afirmativo”. Para una flag binaria que puede llegar como 0/1, mejor boolean.
Comparación entre campos
confirmed (confirmar contraseña)
Espera un segundo campo con sufijo _confirmation. Si validas password, debe existir password_confirmation:
$request->validate([
'password' => 'required|min:8|confirmed',
]);
// el request debe incluir password_confirmation
También puedes indicar un nombre distinto para el campo de confirmación:
$request->validate([
'email' => 'required|email|confirmed:email_repetido',
]);
Si el campo de confirmación no existe en la petición, la validación devuelve un error genérico de “no coincide”, no “falta el campo”. Esa redacción confunde al diagnosticar.
Para confirmar la contraseña actual antes de cambiarla existe current_password, que compara el valor enviado con el hash del usuario autenticado:
$request->validate([
'password_actual' => 'required|current_password',
'nueva_password' => 'required|min:8|confirmed',
]);
// guardia distinta:
'password_actual' => 'required|current_password:api',
Precondición: la regla resuelve Auth::guard($guard)->user() en tiempo de validación. La ruta debe pasar por auth middleware (o por la guardia especificada). Sin sesión autenticada no hay hash con el que comparar y la regla siempre rechaza el campo con el mensaje genérico de “contraseña incorrecta”, ocultando que el problema real es de autenticación.
same y different
same compara dos campos por igualdad; different por desigualdad:
$request->validate([
'nueva_password' => 'required|min:8|different:password_actual',
'nueva_password_confirmacion' => 'required|same:nueva_password',
]);
Diferencia con confirmed: confirmed se declara en el campo origen y busca automáticamente {campo}_confirmation. same se declara en el campo destino y recibe explícitamente el nombre del otro. Con nombres estándar confirmed es más práctico; con nombres arbitrarios, same.
gt, gte, lt, lte
Comparan el valor del campo contra otro campo o un literal. El tipo de comparación depende de la regla de tipo declarada (string = longitud, numeric = valor, array = cantidad, file = KB):
$request->validate([
'precio_minimo' => 'required|numeric|min:0',
'precio_maximo' => 'required|numeric|gt:precio_minimo',
'descuento' => 'required|integer|lte:100',
]);
Importante: los dos campos comparados deben tener la misma regla de tipo. gt:precio_minimo con un campo declarado como string y otro como numeric no funciona como se espera.
Reglas para arrays: distinct y contains
distinct garantiza que no haya duplicados dentro de un array. El escenario es un formulario dinámico donde el usuario añade filas (destinatarios, SKUs):
$request->validate([
'destinatarios' => 'required|array|min:1|max:10',
'destinatarios.*.email' => 'required|email|distinct',
'destinatarios.*.nombre' => 'required|string|max:80',
]);
Sin distinct, dos emails idénticos pasan y el correo se envía dos veces al mismo destinatario.
Por defecto la comparación es laxa. Parámetros:
// "1" y 1 se consideran distintos
'items.*.sku' => 'distinct:strict',
// "Email" y "email" se consideran iguales
'categorias.*' => 'distinct:ignore_case',
contains exige que el array incluya ciertos valores obligatorios:
use Illuminate\Validation\Rule;
$request->validate([
'permisos' => ['required', 'array', Rule::contains(['leer'])],
]);
La guía detallada de array, list, in_array, wildcard * y validación de estructuras anidadas está en arrays y JSON.
Reglas de texto: alpha y regex
La familia alpha acepta caracteres Unicode por defecto: letras acentuadas, cirílico, ideogramas. Para restringir a ASCII puro se añade el parámetro ascii:
$request->validate([
// "María" pasa: Unicode permite la í
'nombre' => 'required|alpha',
// "María" falla: solo a-z y A-Z
'nombre_usuario' => 'required|alpha_dash:ascii',
// letras y números, exactamente 6 caracteres
'codigo' => 'required|alpha_num|size:6',
]);
La familia alpha no permite espacios. Para nombres compuestos como “Ana María” hay que usar regex o simplemente string con max.
Sobre regex: si la expresión contiene |, la sintaxis con pipe se rompe porque Laravel interpreta el | como separador de reglas:
// Roto: Laravel ve "regex:/^(foo" como una regla y "bar)$/" como otra
'codigo' => 'required|regex:/^(foo|bar)$/',
// Correcto: array de reglas, regex como elemento aislado
'codigo' => ['required', 'regex:/^(foo|bar)$/'],
Mismo problema con not_regex. Para patrones con |, array obligatorio.
Para el catálogo completo de string, email, url, uuid, lowercase, uppercase, starts_with, ends_with: la referencia de cadenas y números.
Reglas condicionales entre campos
required_if, required_with, required_without y compañía hacen que un campo sea obligatorio según el estado de otros. La mecánica básica:
$request->validate([
'tipo_envio' => 'required|in:recogida,domicilio',
'direccion' => 'required_if:tipo_envio,domicilio|string|max:400',
'hora_recogida' => 'required_if:tipo_envio,recogida|date_format:H:i',
]);
required_with obliga al campo si al menos uno de los listados está presente y no vacío:
$request->validate([
'latitud' => 'nullable|required_with:longitud|numeric|between:-90,90',
'longitud' => 'nullable|required_with:latitud|numeric|between:-180,180',
]);
El nullable no es decorativo: con el middleware global ConvertEmptyStringsToNull activo por defecto, los inputs en blanco del formulario llegan como null. Sin nullable, la regla numeric se ejecuta sobre null y falla aunque required_with no se haya disparado, rompiendo el caso “usuario dejó ambos campos vacíos”.
La referencia completa de required_unless, required_with_all, required_without, required_without_all, el método $validator->sometimes() y patrones polimórficos están en la guía de validación condicional.
Validación contra base de datos
unique y exists ejecutan queries SQL para verificar valores. Ambas soportan el builder fluent de Rule con filtros, soft deletes y referencias a modelos:
$request->validate([
'email' => ['required', 'email', Rule::unique('usuarios')->ignore($usuario->id)],
'categoria_id' => ['required', Rule::exists('categorias', 'id')->where('activa', true)],
]);
Unicidad multicolumna, soft deletes, conexiones alternativas y condiciones avanzadas con where() están en su propia referencia: unique y exists.
Orden de las reglas y cómo se cortan
Las reglas se evalúan de izquierda a derecha. Hay dos puntos donde la cadena se trunca de forma silenciosa, pero con matices importantes:
nullable permite que el valor sea null sin que fallen las reglas de tipo y formato (string, integer, date, email, regex, unique, exists y similares). No es sensible a la posición, se aplica esté donde esté en el array, y no afecta a las reglas implícitas (required y su familia, present y su familia, accepted, accepted_if, declined, declined_if, filled, missing y su familia), que siguen evaluándose aunque el valor sea null. La familia prohibited* es dependent, no implícita: con nullable y valor null el campo se considera “vacío” y prohibited pasa sin disparar error.
// null pasa: email:rfc y unique se saltan (reglas de formato, no implícitas)
'email_contacto' => 'nullable|email:rfc|unique:usuarios',
// null FALLA: required es implícito y se evalúa aunque nullable esté presente
'rol' => 'required|nullable|string',
sometimes (regla) actúa antes: si la clave no existe en los datos, toda la cadena se salta y el campo no aparece en validated(). No es que la regla falle, es que el campo directamente no participa.
La regla de tipo (string, integer, array, file) determina cómo min, max, size, between interpretan su argumento. Sin tipo, Laravel adivina a partir del valor recibido y los resultados son inesperados:
// string + min:3: al menos 3 caracteres
'nombre' => 'required|string|min:3',
// integer + min:3: valor >= 3
'cantidad' => 'required|integer|min:3',
// sin tipo: si llega "10", min:3 mide longitud del string "10" (2 chars) y falla
'ambiguo' => 'required|min:3',
Regla práctica: declarar siempre el tipo antes de las reglas de tamaño.
integer y numeric aceptan strings que PHP considera numéricas ('42', '3.14') porque internamente usan FILTER_VALIDATE_INT e is_numeric(). Para exigir tipo PHP real existen integer:strict y numeric:strict:
'edad' => 'integer', // acepta 42 y "42"
'edad' => 'integer:strict', // solo entero PHP
'monto' => 'numeric', // acepta 3.14, "3.14"
'monto' => 'numeric:strict', // solo int/float PHP
Cuándo usar qué regla por tipo de dato
Orientación rápida para elegir entre familias. Cada tipo tiene su propia guía con análisis en profundidad.
- Strings y números: empezar con
stringointeger/numeric. Para preciosdecimal:2. Para email mínimoemail:rfc, sin parámetros la validación es demasiado permisiva. Detalle en cadenas y números. - Fechas:
dateacepta cualquier formato que entiendastrtotime();date_formatexige uno concreto. Para comparaciones (after,before) puede referenciarse otro campo. Detalle en fechas. - Archivos:
fileoimage; para contenido realmimes/mimetypes; para extensión declaradaextensions. Detalle en archivos. - Arrays y JSON:
array,list(solo índices consecutivos),json(solo sintaxis). Wildcard*para elementos individuales. Detalle en arrays y JSON. - Contraseñas:
Password::min(8)->letters()->numbers()yuncompromised(). Detalle en contraseñas.
Validación dinámica con Rule::when
Cuando las reglas dependen del usuario, de feature flags o del estado de otro registro, Rule::when() evita tener que construir el array de reglas en un if externo:
use Illuminate\Validation\Rule;
$request->validate([
'descuento' => [
'required',
'integer',
'min:0',
Rule::when(
$request->user()->esAdmin(),
['max:100'],
['max:30'],
),
],
]);
Admin puede dar hasta 100% de descuento; el resto, hasta 30. Las dos ramas reciben arrays de reglas, no reglas sueltas.
Patrón habitual para create vs update:
'email' => [
'required',
'email:rfc',
Rule::when(
null === $this->route('usuario'),
[Rule::unique('usuarios')],
[Rule::unique('usuarios')->ignore($this->route('usuario'))],
),
],
Rule::when() resuelve la condición al construirse el array de reglas. Cuando la condición es un booleano, PHP la evalúa de inmediato. Cuando es un closure, Laravel la ejecuta al compilar las reglas, pero antes de fails() o passes().
Para condiciones que dependen del input ya recibido (no de auth o config), el método $validator->sometimes() da acceso al payload:
$validator = Validator::make($datos, [
'tipo_pago' => 'required|in:tarjeta,transferencia',
'numero_cuenta' => 'string|size:20',
]);
$validator->sometimes('numero_cuenta', 'required', function ($input) {
return 'transferencia' === $input->tipo_pago;
});
Reglas estáticas en el array, reglas dependientes del payload con sometimes(). La guía de validación condicional cubre los patrones polimórficos completos.
Tabla de referencia rápida
Lista de reglas agrupadas por función. Para localizar rápido qué regla aplica a qué situación.
Presencia y obligatoriedad
| Regla | Función |
|---|---|
required | Clave presente y valor no vacío |
filled | No vacío si la clave está presente |
present | La clave debe existir (cualquier valor) |
nullable | Permite null en reglas de tipo y formato; no afecta a required, present, accepted |
sometimes | Validar solo si la clave existe en los datos |
missing | La clave no debe aparecer |
prohibited | La clave no debe aparecer o debe estar vacía |
Condicionales (presencia según otro campo)
| Regla | Función |
|---|---|
required_if | Obligatorio si otro campo tiene un valor |
required_unless | Obligatorio salvo cuando otro campo tiene un valor |
required_with | Obligatorio si alguno de los listados existe |
required_without | Obligatorio si alguno de los listados falta |
required_if_accepted | Obligatorio si otro campo es “accepted” |
prohibited_if | Prohibido si otro campo tiene un valor |
prohibited_unless | Prohibido salvo en un valor concreto |
exclude_if | Excluir del resultado de validated() |
exclude_unless | Incluir en validated() solo en un caso |
present_if / missing_if | Variantes condicionales sobre la clave |
Tipo de valor
| Regla | Función |
|---|---|
string | Cadena de texto |
integer / integer:strict | Entero (acepta string numérica o no) |
numeric / numeric:strict | Número (acepta string numérica o no) |
boolean / boolean:strict | Booleano o convertible |
array | Array PHP (admite lista de claves permitidas) |
list | Array con índices consecutivos 0..n |
json | Cadena JSON sintácticamente válida |
file | Archivo subido correctamente |
date / date_format | Fecha (libre o con formato fijo) |
Comparación entre campos
| Regla | Función |
|---|---|
confirmed | Coincide con {campo}_confirmation (o nombre custom) |
same:otro | Coincide con el campo indicado |
different:otro | Distinto al campo indicado |
gt:otro / gte:otro | Mayor / mayor o igual |
lt:otro / lte:otro | Menor / menor o igual |
current_password | Coincide con hash del usuario autenticado |
Restricción de valores
| Regla | Función |
|---|---|
in:a,b,c | Pertenece a la lista |
not_in:a,b,c | No pertenece a la lista |
Rule::enum(Foo::class) | Valor de un backed enum |
Rule::anyOf([...]) | Pasa al menos un conjunto de reglas |
accepted | yes/on/true/1/”1”/“true” |
declined | no/off/false/0/”0”/“false” |
Tamaño y rango
| Regla | Función |
|---|---|
min:n / max:n | Mínimo / máximo (chars, valor, elementos, KB según tipo) |
size:n | Tamaño exacto |
between:min,max | Rango inclusivo |
digits:n / digits_between:min,max | Cantidad exacta o rango de dígitos |
min_digits:n / max_digits:n | Mínimo / máximo de dígitos |
Strings
| Regla | Función |
|---|---|
alpha / alpha:ascii | Solo letras (Unicode o ASCII) |
alpha_dash / alpha_dash:ascii | Letras, dígitos, guión, guión bajo |
alpha_num / alpha_num:ascii | Letras y dígitos |
regex:patron / not_regex:patron | Coincide / no coincide con expresión regular |
starts_with / ends_with | Empieza / termina con valor |
email / url / uuid / ulid / ip | Formatos comunes |
lowercase / uppercase / ascii | Restricciones de caracteres |
Arrays
| Regla | Función |
|---|---|
array / array:keys | Array, opcional con claves permitidas |
list | Array con índices 0..n consecutivos |
distinct / distinct:strict / distinct:ignore_case | Sin duplicados |
in_array:otro.* | Valor presente en otro array del request |
Rule::contains([...]) | El array contiene todos los valores listados |
required_array_keys:a,b | Array que incluye al menos las claves listadas |
Control de flujo
| Regla / Modificador | Función |
|---|---|
bail | Detener el campo en el primer error |
exclude | Excluir del resultado de validated() siempre |
stopOnFirstFailure() (método) | Detener la validación entera en el primer error |
#[StopOnFirstFailure] (atributo) | Igual, en Form Request |
Patrones de uso frecuente
Combinaciones que aparecen en proyecto tras proyecto.
PATCH donde el cliente envía solo los campos modificados:
public function rules(): array
{
return [
'nombre' => 'sometimes|required|string|max:100',
'email' => ['sometimes', 'required', 'email', Rule::unique('usuarios')->ignore($this->user()->id)],
'bio' => 'sometimes|nullable|string|max:500',
];
}
Formularios donde las secciones dependen del tipo de entidad:
$request->validate([
'tipo' => 'required|in:persona,empresa',
'rfc' => 'required_if:tipo,empresa|prohibited_if:tipo,persona|string|size:13',
'curp' => 'required_if:tipo,persona|prohibited_if:tipo,empresa|string|size:18',
]);
bail antes de reglas que tocan la base de datos:
$request->validate([
'email' => 'bail|required|email:rfc|unique:usuarios',
'codigo_invitacion' => 'bail|required|string|exists:invitaciones,codigo',
]);
Cuando las reglas crecen y dejan de caber en el controlador, lo siguiente es moverlas a un Form Request. Si las reglas integradas no cubren tu caso, puedes crear reglas propias. El manejo de textos de error tiene su referencia, y la guía rápida cubre los fundamentos desde cero.
¿Te ha resultado útil este artículo?
¡Gracias por tu opinión!