wppw.org
enrues
Laravel 13.x · Docs

Reglas de validación | Laravel 13.x en español - Referencia con diferencias clave

Actualizado 25 de mayo de 2026 Laravel 13.x 25 min lectura
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:

  • sometimes actúa sobre la presencia de la clave. Si el campo no existe en el request, ninguna regla se ejecuta.
  • nullable actúa sobre el valor. La clave existe (o el campo es required), pero null se 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 string o integer/numeric. Para precios decimal:2. Para email mínimo email:rfc, sin parámetros la validación es demasiado permisiva. Detalle en cadenas y números.
  • Fechas: date acepta cualquier formato que entienda strtotime(); date_format exige uno concreto. Para comparaciones (after, before) puede referenciarse otro campo. Detalle en fechas.
  • Archivos: file o image; para contenido real mimes/mimetypes; para extensión declarada extensions. 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() y uncompromised(). 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

ReglaFunción
requiredClave presente y valor no vacío
filledNo vacío si la clave está presente
presentLa clave debe existir (cualquier valor)
nullablePermite null en reglas de tipo y formato; no afecta a required, present, accepted
sometimesValidar solo si la clave existe en los datos
missingLa clave no debe aparecer
prohibitedLa clave no debe aparecer o debe estar vacía

Condicionales (presencia según otro campo)

ReglaFunción
required_ifObligatorio si otro campo tiene un valor
required_unlessObligatorio salvo cuando otro campo tiene un valor
required_withObligatorio si alguno de los listados existe
required_withoutObligatorio si alguno de los listados falta
required_if_acceptedObligatorio si otro campo es “accepted”
prohibited_ifProhibido si otro campo tiene un valor
prohibited_unlessProhibido salvo en un valor concreto
exclude_ifExcluir del resultado de validated()
exclude_unlessIncluir en validated() solo en un caso
present_if / missing_ifVariantes condicionales sobre la clave

Tipo de valor

ReglaFunción
stringCadena de texto
integer / integer:strictEntero (acepta string numérica o no)
numeric / numeric:strictNúmero (acepta string numérica o no)
boolean / boolean:strictBooleano o convertible
arrayArray PHP (admite lista de claves permitidas)
listArray con índices consecutivos 0..n
jsonCadena JSON sintácticamente válida
fileArchivo subido correctamente
date / date_formatFecha (libre o con formato fijo)

Comparación entre campos

ReglaFunción
confirmedCoincide con {campo}_confirmation (o nombre custom)
same:otroCoincide con el campo indicado
different:otroDistinto al campo indicado
gt:otro / gte:otroMayor / mayor o igual
lt:otro / lte:otroMenor / menor o igual
current_passwordCoincide con hash del usuario autenticado

Restricción de valores

ReglaFunción
in:a,b,cPertenece a la lista
not_in:a,b,cNo pertenece a la lista
Rule::enum(Foo::class)Valor de un backed enum
Rule::anyOf([...])Pasa al menos un conjunto de reglas
acceptedyes/on/true/1/”1”/“true”
declinedno/off/false/0/”0”/“false”

Tamaño y rango

ReglaFunción
min:n / max:nMínimo / máximo (chars, valor, elementos, KB según tipo)
size:nTamaño exacto
between:min,maxRango inclusivo
digits:n / digits_between:min,maxCantidad exacta o rango de dígitos
min_digits:n / max_digits:nMínimo / máximo de dígitos

Strings

ReglaFunción
alpha / alpha:asciiSolo letras (Unicode o ASCII)
alpha_dash / alpha_dash:asciiLetras, dígitos, guión, guión bajo
alpha_num / alpha_num:asciiLetras y dígitos
regex:patron / not_regex:patronCoincide / no coincide con expresión regular
starts_with / ends_withEmpieza / termina con valor
email / url / uuid / ulid / ipFormatos comunes
lowercase / uppercase / asciiRestricciones de caracteres

Arrays

ReglaFunción
array / array:keysArray, opcional con claves permitidas
listArray con índices 0..n consecutivos
distinct / distinct:strict / distinct:ignore_caseSin duplicados
in_array:otro.*Valor presente en otro array del request
Rule::contains([...])El array contiene todos los valores listados
required_array_keys:a,bArray que incluye al menos las claves listadas

Control de flujo

Regla / ModificadorFunción
bailDetener el campo en el primer error
excludeExcluir 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?

Reportar un error