Validación condicional en Laravel 13
Los formularios rara vez tienen una estructura fija. Un campo de dirección aparece solo si el envío es a domicilio. El NIF de empresa se requiere si el tipo de cliente es “empresa”. Un campo de teléfono es obligatorio cuando no se proporciona email. Laravel tiene un conjunto de reglas condicionales que controlan cuándo un campo es obligatorio, cuándo se valida y cuándo se excluye del resultado.
Para los conceptos generales de validación, consulta la guía de inicio rápido. Las reglas exclude_* aplicadas a arrays con comodín se cubren en el artículo de arrays y JSON.
La familia required_*
required_if – obligatorio cuando otro campo tiene un valor
La regla required_if hace que un campo sea obligatorio cuando otro campo tiene un valor específico:
$request->validate([
'tipo_cliente' => 'required|in:particular,empresa',
'nif_empresa' => 'required_if:tipo_cliente,empresa|string|max:20',
]);
Si tipo_cliente es empresa, el campo nif_empresa es obligatorio. Si es particular, se ignora. La comparación es case-sensitive – required_if:tipo_cliente,empresa no se activa si el valor es Empresa con mayúscula.
Para múltiples valores (lógica OR):
// Obligatorio si el estado es "activo" O "pendiente"
'motivo' => 'required_if:estado,activo,pendiente'
Los valores se listan separados por comas después del nombre del campo. Si el campo debe ser obligatorio con una condición más compleja, Rule::requiredIf() acepta un booleano o un closure:
use Illuminate\Validation\Rule;
$request->validate([
'justificacion' => [
Rule::requiredIf(function () use ($request) {
return 'empresa' === $request->tipo_cliente
&& $request->monto > 10000;
}),
'nullable',
'string',
'max:500',
],
]);
El closure permite combinar varias condiciones que no se pueden expresar con la sintaxis de string. Rule::requiredIf() también acepta un booleano directamente:
'justificacion' => Rule::requiredIf($request->monto > 10000),
required_if_accepted y required_if_declined
Variantes especializadas para campos tipo checkbox o toggle:
$request->validate([
'acepta_envio' => 'sometimes|boolean',
'direccion' => 'required_if_accepted:acepta_envio|string|max:255',
'codigo_postal' => 'required_if_accepted:acepta_envio|string|size:5',
]);
required_if_accepted se activa cuando el campo referenciado es "yes", "on", 1, "1", true o "true". Su inversa required_if_declined se activa con "no", "off", 0, "0", false o "false".
required_unless – obligatorio a menos que
La inversa de required_if. El campo es obligatorio a menos que otro campo tenga uno de los valores indicados:
$request->validate([
'rol' => 'required|in:admin,editor,lector',
'departamento' => 'required_unless:rol,admin|string',
]);
El administrador no necesita departamento – tiene acceso global. Editores y lectores sí.
La forma fluent Rule::requiredUnless() funciona igual que Rule::requiredIf():
'codigo_descuento' => Rule::requiredUnless($request->user()->es_premium),
Un caso especial: required_unless:campo,null hace el campo obligatorio a menos que el otro campo sea null o no esté en el request. Es útil para campos que dependen de una relación opcional.
Con required_unless también se puede exigir un campo a menos que otro tenga cualquiera de varios valores:
// Obligatorio a menos que el plan sea "gratis" o "trial"
'metodo_pago' => 'required_unless:plan,gratis,trial|string'
required_with – obligatorio si otro campo está presente
required_with hace el campo obligatorio cuando cualquiera de los campos listados está presente y no está vacío:
$request->validate([
'telefono' => 'nullable|string',
'extension' => 'required_with:telefono|string|max:10',
]);
La extensión solo es obligatoria si se proporcionó un teléfono. Si telefono llega vacío o no se envía, extension es opcional.
Para exigir que el campo esté presente cuando todos los campos listados existen, required_with_all:
$request->validate([
'calle' => 'nullable|string',
'ciudad' => 'nullable|string',
'pais' => 'nullable|string',
'codigo_postal' => 'required_with_all:calle,ciudad,pais|string|size:5',
]);
El código postal solo se exige cuando calle, ciudad y país están presentes. Si falta alguno de los tres, el código postal es opcional.
required_without – obligatorio si otro campo no está presente
required_without hace el campo obligatorio cuando cualquiera de los campos listados está vacío o ausente:
$request->validate([
'email' => 'required_without:telefono|email',
'telefono' => 'required_without:email|string',
]);
El usuario debe proporcionar al menos un dato de contacto. Si no hay email, se exige teléfono, y viceversa. Si ambos están presentes, ambos se validan.
required_without_all exige el campo cuando todos los campos listados están vacíos:
$request->validate([
'email' => 'nullable|email',
'telefono' => 'nullable|string',
'direccion' => 'nullable|string',
'contacto_alternativo' => 'required_without_all:email,telefono,direccion|string',
]);
Solo se exige contacto_alternativo cuando no se proporciona ninguna de las otras tres opciones de contacto.
Un error frecuente: confundir required_without con la negación de required_if. required_without verifica si el campo está presente en el request, no si su valor es falsy. Un campo enviado con valor vacío ("") cuenta como “no presente” para required_without, pero un campo enviado con valor "0" o "false" cuenta como “presente”.
Resumen de la familia required_*
| Regla | El campo es obligatorio cuando… |
|---|---|
required_if:campo,valor | campo tiene el valor indicado |
required_unless:campo,valor | campo NO tiene el valor indicado |
required_with:a,b | cualquiera de a, b está presente |
required_with_all:a,b | a Y b están presentes |
required_without:a,b | cualquiera de a, b está ausente |
required_without_all:a,b | a Y b están ausentes |
required_if_accepted:campo | campo es truthy (yes, on, 1, true) |
required_if_declined:campo | campo es falsy (no, off, 0, false) |
La diferencia clave: required_with y required_without operan sobre la presencia del campo, no sobre su valor. required_if y required_unless operan sobre el valor del campo.
Validar checkboxes
Los checkboxes en HTML no se envían cuando no están marcados – el campo directamente no aparece en el request. Esto requiere un patrón específico.
Para un checkbox obligatorio (aceptar términos):
$request->validate([
'acepto_terminos' => 'required|accepted',
]);
accepted valida que el valor sea "yes", "on", 1, "1", true o "true". required asegura que el campo esté presente. Si el formulario permite proceder sin aceptar (un aviso, no un requisito legal), quitar required y usar solo sometimes|accepted.
Para un checkbox opcional cuyo valor se procesa solo si se marca:
$request->validate([
'suscribir_newsletter' => 'sometimes|accepted',
]);
sometimes hace que la validación solo se ejecute si el campo está en el request. Si el checkbox no se marca, el campo no se envía y sometimes lo ignora.
Para guardar el valor de un checkbox opcional como booleano, prepareForValidation en un Form Request normaliza el campo:
protected function prepareForValidation(): void
{
$this->merge([
'suscribir_newsletter' => $this->has('suscribir_newsletter'),
]);
}
Esto convierte la presencia/ausencia del campo en true/false, y se puede validar con 'suscribir_newsletter' => 'required|boolean'.
Excluir campos: exclude_if y exclude_unless
Las reglas exclude_* no solo controlan si un campo es obligatorio – lo eliminan completamente de los datos validados. Los campos excluidos no aparecen en el resultado de validated() ni safe().
$request->validate([
'tiene_cita' => 'required|boolean',
'fecha_cita' => 'exclude_if:tiene_cita,false|required|date',
'nombre_medico' => 'exclude_if:tiene_cita,false|required|string',
]);
$datos = $request->validated();
// Si tiene_cita es false: $datos no tiene fecha_cita ni nombre_medico
exclude_unless es la inversa – excluye a menos que el otro campo tenga el valor indicado:
'fecha_cita' => 'exclude_unless:tiene_cita,true|required|date',
Ambas producen el mismo resultado en este ejemplo, pero la semántica es distinta. Elegir la que se lea de forma más natural.
La exclusión ocurre antes de que las demás reglas del campo se evalúen. Si exclude_if determina que el campo se excluye, required no falla – el campo simplemente desaparece.
Un patrón para formularios polimórficos (el mismo formulario crea distintos tipos de entidad):
$request->validate([
'tipo' => 'required|in:persona,empresa',
// Campos de persona
'nombre' => 'exclude_unless:tipo,persona|required|string|max:100',
'apellido' => 'exclude_unless:tipo,persona|required|string|max:100',
// Campos de empresa
'razon_social' => 'exclude_unless:tipo,empresa|required|string|max:200',
'nif' => 'exclude_unless:tipo,empresa|required|string|max:20',
]);
$datos = $request->validated();
// Si tipo=persona: $datos tiene nombre, apellido; NO tiene razon_social, nif
// Si tipo=empresa: $datos tiene razon_social, nif; NO tiene nombre, apellido
La forma fluent Rule::excludeIf() acepta un booleano o closure:
'campo_admin' => [
Rule::excludeIf(! $request->user()->esAdmin()),
'required',
'string',
],
Un punto importante: si el código downstream accede a una clave que fue excluida ($datos['razon_social'] cuando tipo es persona), obtiene un error porque la clave no existe. Usar $datos['razon_social'] ?? null o verificar con isset().
Las reglas exclude_with y exclude_without proporcionan variantes basadas en la presencia del campo (no su valor):
$request->validate([
'cupon' => 'nullable|string',
// Excluir descuento_manual si se proporcionó cupón
'descuento_manual' => 'exclude_with:cupon|nullable|numeric|min:0',
]);
exclude_with:cupon excluye el campo si cupon está presente en el request. exclude_without hace lo inverso – excluye si el campo referenciado no está presente. Estas reglas se cubren también en el artículo de arrays y JSON para el caso de arrays con comodín.
Rule::when() – añadir reglas dinámicamente
Rule::when() añade reglas al array solo si una condición se cumple:
use Illuminate\Validation\Rule;
$request->validate([
'email' => [
'required',
'email',
Rule::when($request->isMethod('POST'), 'unique:usuarios,email'),
Rule::when($request->isMethod('PUT'), Rule::unique('usuarios', 'email')->ignore($usuario)),
],
]);
El primer argumento es un booleano o closure. El segundo son las reglas que se añaden si la condición es true. Un tercer argumento opcional son las reglas que se añaden si la condición es false.
Rule::when() se evalúa cuando se construye el array de reglas (en el método rules() del Form Request), no durante la validación. Esto significa que se puede usar con datos del request y del usuario autenticado, pero no con valores de otros campos que aún no se han validado.
Un uso práctico – reglas distintas para crear y actualizar en un solo Form Request:
public function rules(): array
{
$esCreacion = null === $this->route('producto');
return [
'nombre' => 'required|string|max:200',
'sku' => [
'required',
'string',
Rule::when($esCreacion, 'unique:productos,sku'),
Rule::when(! $esCreacion, Rule::unique('productos', 'sku')->ignore($this->route('producto'))),
],
'precio' => 'required|numeric|min:0',
];
}
Rule::when() con un tercer argumento permite definir reglas para el caso false:
'campo' => [
Rule::when(
$condicion,
['required', 'string'], // Si true
['nullable', 'string'], // Si false
),
],
El método sometimes() del Validator
Para condiciones que dependen de los datos ya presentes en el validador, el método sometimes() añade reglas de forma programática:
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'partidas' => 'required|integer|min:0',
]);
$validator->sometimes('motivo', 'required|string|max:500', function ($input) {
return $input->partidas >= 100;
});
Si partidas es 100 o más, el campo motivo se vuelve obligatorio con las reglas indicadas. El closure recibe una instancia de Fluent con los datos del request.
Se pueden añadir reglas a varios campos a la vez:
$validator->sometimes(['motivo', 'presupuesto'], 'required', function ($input) {
return $input->partidas >= 100;
});
sometimes() en arrays
Para validación condicional dentro de arrays anidados, el closure recibe un segundo argumento con el elemento actual:
$validator->sometimes('canales.*.direccion', 'email', function ($input, $item) {
return 'email' === $item->tipo;
});
$validator->sometimes('canales.*.direccion', 'url', function ($input, $item) {
return 'email' !== $item->tipo;
});
Cada elemento del array canales se evalúa individualmente. Si el tipo es email, la dirección se valida como email. Si es otro tipo, se valida como URL. $item es una instancia de Fluent con los datos del elemento actual.
Formularios multi-paso
En formularios que se completan en varios pasos, cada paso valida solo los campos de esa etapa. La validación condicional permite manejar esto en un solo Form Request:
public function rules(): array
{
$paso = (int) $this->input('paso', 1);
$reglas = [
'paso' => 'required|integer|in:1,2,3',
];
if ($paso >= 1) {
$reglas['nombre'] = 'required|string|max:100';
$reglas['email'] = 'required|email';
}
if ($paso >= 2) {
$reglas['direccion'] = 'required|string|max:255';
$reglas['ciudad'] = 'required|string|max:100';
}
if (3 === $paso) {
$reglas['metodo_pago'] = 'required|in:tarjeta,transferencia';
$reglas['acepto_terminos'] = 'required|accepted';
}
return $reglas;
}
Cada paso acumula las reglas de los pasos anteriores, lo que garantiza que los datos del paso 1 siguen siendo válidos cuando se envía el paso 3. Una alternativa más limpia es tener un Form Request por paso, lo que separa las responsabilidades pero requiere más archivos.
Con Rule::when() el mismo patrón se puede escribir sin condicionales:
return [
'paso' => 'required|integer|in:1,2,3',
'nombre' => Rule::when($paso >= 1, 'required|string|max:100'),
'email' => Rule::when($paso >= 1, 'required|email'),
'direccion' => Rule::when($paso >= 2, 'required|string|max:255'),
'metodo_pago' => Rule::when(3 === $paso, 'required|in:tarjeta,transferencia'),
];
Errores frecuentes
required_if case-sensitive – required_if:tipo,admin no se activa si el valor es Admin o ADMIN. La comparación es literal. Si los valores pueden variar en capitalización, normalizar en prepareForValidation o usar Rule::requiredIf() con un closure que compare con strtolower().
Confundir required_with con required_with_all – required_with:a,b exige el campo si a O b están presentes. required_with_all:a,b lo exige si a Y b están presentes. La distinción es sutil pero cambia el comportamiento.
exclude_if elimina la clave de validated() – código que acceda a $datos['campo_excluido'] sin verificación obtendrá un error. Usar el operador null-safe o isset().
required_without vs “requerido si el campo es falsy” – required_without:campo verifica si campo está presente en el request (existe como clave), no si su valor es verdadero. Un campo con valor "" se considera ausente para required_without, pero un campo con valor 0 se considera presente.
Rule::when() vs sometimes() – Rule::when() se evalúa al construir las reglas (antes de la validación). sometimes() se evalúa durante la validación, cuando ya se tiene acceso a los datos parseados. Para condiciones que dependen de valores del input ya parseados, sometimes() es más fiable.
Múltiples required_if con pipe – 'campo' => 'required_if:tipo,a|required_if:tipo,b' no produce lógica OR como se esperaría. Cada regla se evalúa independientemente y ambas generan mensajes de error si fallan. Para OR con múltiples valores, usar la sintaxis de comas: required_if:tipo,a,b. Para condiciones complejas, Rule::requiredIf() con closure.
Checkbox no marcado = campo ausente – un checkbox HTML no marcado no se envía en el request. Las reglas required fallan porque el campo no existe. Usar sometimes|accepted para checkboxes opcionales o normalizar el campo en prepareForValidation.
exclude_if con campo ausente vs null – exclude_if:campo,null funciona cuando campo está presente en el request con valor PHP null (Laravel convierte internamente el string "null" a null para la comparación). Pero si campo no está en el request en absoluto (la clave no existe), exclude_if no se activa. La distinción es entre “campo presente con valor null” y “campo ausente”. Para el segundo caso, usar exclude_without:campo.
Formulario multi-paso con datos perdidos – si cada paso envía solo sus campos, los datos del paso 1 no están disponibles en el paso 2. La validación acumulativa necesita que todos los datos estén en el request (almacenados en sesión o en campos ocultos) o que cada paso se valide de forma independiente. Mezclar ambos enfoques causa errores difíciles de diagnosticar.
Para la lista completa de reglas, consulta la referencia de reglas. La personalización de mensajes se cubre en mensajes de error. Las reglas de exclusión en arrays se tratan en arrays y JSON.