Curso de Laravel

Validación de formularios en Laravel

Por Víctor Peña · Publicado el

Hola, ¿cómo están? Cerramos el módulo de la aplicación web con la validación de formularios.

Es la frontera entre tu sistema y todo lo que no controlas, así que conviene tratarla con seriedad.

¡Empecemos!

Validar en el controlador

La forma más directa:

public function store(Request $request)
{
    $datos = $request->validate([
        'cedula' => 'required|string|max:20|unique:pacientes',
        'nombre' => 'required|string|max:100',
        'apellido' => 'required|string|max:100',
        'fecha_nacimiento' => 'required|date|before:today',
        'correo' => 'nullable|email|max:255',
        'telefono' => 'nullable|string|max:20',
    ]);

    Paciente::create($datos);

    return redirect()->route('pacientes.index')
        ->with('exito', 'Paciente registrado');
}

Aquí ocurren tres cosas que conviene entender:

Si la validación falla, Laravel detiene la ejecución, redirige al formulario anterior con los errores y con los datos que el usuario escribió. No tienes que hacer nada.

Si pasa, validate() devuelve solo los campos validados. Eso es importante: si alguien añade un campo rol a la petición, no llega al create().

Los datos devueltos son seguros para asignación masiva, porque son exactamente los que declaraste.

Por eso conviene usar $datos y no $request->all().

Las reglas más usadas

'required'              // obligatorio
'nullable'              // puede venir vacío
'string'
'integer'
'numeric'
'boolean'
'array'
'email'
'url'
'date'
'image'
'file'

'max:100'               // longitud o valor máximo
'min:3'
'between:1,10'
'size:8'

'unique:pacientes'      // no existe en esa tabla
'unique:pacientes,cedula'
'exists:doctores,id'    // sí existe

'confirmed'             // debe coincidir con campo_confirmation
'same:otro_campo'
'different:otro_campo'

'in:programada,atendida,cancelada'
'not_in:admin'

'before:today'
'after:2026-01-01'
'after_or_equal:fecha_inicio'

'regex:/^[0-9]{7,8}$/'

Se pueden escribir como texto separado por barras o como arreglo, que es más legible cuando son muchas:

'cedula' => ['required', 'string', 'max:20', 'unique:pacientes'],

Y para ver todas las disponibles, la documentación oficial de validación es de las mejores páginas del framework.

Form Requests: sacar la validación del controlador

Cuando las reglas crecen, el controlador se llena. La solución del framework es una clase aparte:

php artisan make:request GuardarPacienteRequest
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class GuardarPacienteRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'cedula' => ['required', 'string', 'max:20', 'unique:pacientes'],
            'nombre' => ['required', 'string', 'max:100'],
            'apellido' => ['required', 'string', 'max:100'],
            'fecha_nacimiento' => ['required', 'date', 'before:today'],
            'correo' => ['nullable', 'email', 'max:255'],
        ];
    }

    public function messages(): array
    {
        return [
            'cedula.unique' => 'Ya existe un paciente con esa cédula.',
            'fecha_nacimiento.before' => 'La fecha de nacimiento no puede ser futura.',
        ];
    }

    public function attributes(): array
    {
        return [
            'fecha_nacimiento' => 'fecha de nacimiento',
        ];
    }
}

Y el controlador queda así:

public function store(GuardarPacienteRequest $request)
{
    Paciente::create($request->validated());

    return redirect()->route('pacientes.index')
        ->with('exito', 'Paciente registrado');
}

Dos líneas. La validación se ejecuta antes de entrar al método, así que si llegas ahí es porque los datos son correctos.

Esto es lo que mencionábamos en la lección de controladores sobre mantenerlos delgados.

El método authorize

Ese método decide quién puede hacer la petición:

public function authorize(): bool
{
    return $this->user()->esAdmin();
}

Si devuelve false, Laravel responde con un 403 antes de validar nada. Lo veremos a fondo en la lección de autorización.

Mostrar los errores en Blade

Un resumen general:

@if ($errors->any())
    <div class="alerta-error">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

O por campo, que es lo recomendable porque el usuario ve el error junto al campo que debe corregir:

<input type="text" name="cedula" value="{{ old('cedula') }}"
       class="@error('cedula') campo-error @enderror">

@error('cedula')
    <span class="mensaje-error">{{ $message }}</span>
@enderror

old('cedula') es imprescindible. Sin él, el formulario se vacía al fallar la validación y el usuario tiene que reescribirlo todo. Es de las cosas que más molestan de un sistema mal hecho.

Si construiste el componente de campo de la lección anterior, todo esto ya está resuelto dentro y no lo repites nunca más.

Traducir los mensajes

Por defecto los mensajes vienen en inglés. Para tenerlos en español:

php artisan lang:publish

Eso crea lang/en/validation.php. Copia la carpeta a lang/es/, traduce los mensajes y cambia el idioma en config/app.php:

'locale' => 'es',

Lo desarrollamos en la lección de traducir mensajes.

Validación en la actualización

Aquí hay un detalle que hace fallar a mucha gente.

La regla unique falla al editar, porque el propio registro ya tiene esa cédula. Hay que excluirlo:

use Illuminate\Validation\Rule;

public function rules(): array
{
    return [
        'cedula' => [
            'required',
            'max:20',
            Rule::unique('pacientes')->ignore($this->paciente),
        ],
    ];
}

Ese $this->paciente es el modelo que llega por la vinculación de rutas.

Lo habitual es tener dos Form Requests, GuardarPacienteRequest y ActualizarPacienteRequest, precisamente por esta diferencia.

Validación condicional

Cuando una regla depende de otro campo:

public function rules(): array
{
    return [
        'tipo_pago' => ['required', 'in:efectivo,tarjeta'],
        'numero_tarjeta' => ['required_if:tipo_pago,tarjeta', 'digits:16'],
        'observaciones' => ['required_without:diagnostico'],
    ];
}

Y para casos más complejos:

public function withValidator($validator): void
{
    $validator->sometimes('descuento', 'max:20', function ($datos) {
        return $datos->total < 1000;
    });
}

Validar arreglos

Para formularios con varios elementos, como las líneas de una factura:

'items' => ['required', 'array', 'min:1'],
'items.*.producto_id' => ['required', 'exists:productos,id'],
'items.*.cantidad' => ['required', 'integer', 'min:1'],

El * aplica la regla a cada elemento del arreglo.

Reglas personalizadas

Cuando necesitas una validación propia:

php artisan make:rule CedulaBoliviana
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (! preg_match('/^\d{6,8}(-[A-Z]{2})?$/', $value)) {
        $fail('El campo :attribute no tiene un formato de cédula válido.');
    }
}
'cedula' => ['required', new CedulaBoliviana],

Para casos simples también sirve una función anónima directamente en las reglas.

Validar en una API

Cuando la petición espera JSON, Laravel devuelve los errores en JSON con código 422 automáticamente:

{
    "message": "The given data was invalid.",
    "errors": {
        "cedula": ["El campo cédula es obligatorio."]
    }
}

No hay que cambiar nada: el mismo Form Request sirve para web y para API.

La regla que no cambia

Como advertimos en el curso de PHP: la validación del navegador no cuenta.

Los atributos required, type="email" y pattern mejoran la experiencia del usuario, pero cualquiera puede saltárselos enviando la petición desde otra herramienta.

Toda validación del cliente debe repetirse en el servidor. La del navegador es comodidad; la de Laravel es seguridad.

Errores comunes

  • Usar $request->all() en lugar de los datos validados.
  • Olvidar old() y vaciar el formulario al fallar.
  • unique sin excluir el registro al actualizar.
  • Validar a mano con if en lugar de usar las reglas.
  • Confiar en la validación del navegador.
  • Reglas larguísimas en el controlador en vez de un Form Request.

Para cerrar

La validación es lo que impide que datos incorrectos entren a tu sistema, y Laravel la resuelve con muy poco código.

Los dos hábitos: Form Request en cuanto pases de cuatro reglas, y usar siempre validated() en lugar de all().

Con esto cerramos el módulo. En la siguiente lección juntamos rutas, controladores, Blade y validación en un CRUD completo.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp