12 - Guía Completa

 Introducción

Las validaciones son un componente crítico en cualquier aplicación web moderna. Laravel 12 ofrece un sistema de validación robusto y flexible que te permite proteger tu aplicación contra ataques de seguridad como SQL Injection, XSS y CSRF, además de garantizar la integridad de los datos.

En este tutorial, exploraremos todas las capas de validación disponibles en Laravel 12, desde la validación en el cliente hasta las validaciones a nivel de servidor, con ejemplos prácticos y actualizados para la versión 12.

Índice

  1. Tipos de Validación

  2. Validación en el Cliente

  3. Validación con JavaScript y jQuery

  4. Validación en el Servidor con Form Requests

  5. Validación Directa en Controladores

  6. Mensajes de Error Personalizados

  7. Validaciones Personalizadas

  8. Validación en Tiempo Real

  9. Buenas Prácticas


1. Tipos de Validación

En una aplicación Laravel, podemos implementar validaciones en diferentes capas:

CapaTecnologíaPropósito
ClienteHTML5, JavaScriptExperiencia de usuario inmediata
Cliente AvanzadojQuery, PluginsValidaciones más complejas en frontend
ServidorLaravel RequestSeguridad e integridad de datos
Base de DatosMigraciones, ModelosIntegridad referencial y constraints

2. Validación en el Cliente

HTML5 Validation

La validación HTML5 es la primera línea de defensa y mejora la experiencia del usuario:

blade
<form action="{{ route('pasteles.store') }}" method="POST" novalidate>
    @csrf
    
    <!-- Validación básica con required -->
    <div class="mb-3">
        <label for="nombre" class="form-label">Nombre del Pastel *</label>
        <input type="text" 
               class="form-control" 
               id="nombre" 
               name="nombre"
               required
               minlength="3"
               maxlength="100"
               placeholder="Ej: Pastel de Chocolate">
    </div>

    <!-- Validación con pattern (expresión regular) -->
    <div class="mb-3">
        <label for="codigo" class="form-label">Código del Producto</label>
        <input type="text" 
               class="form-control" 
               id="codigo" 
               name="codigo"
               pattern="[A-Z]{3}-\d{4}"
               title="Formato: ABC-1234"
               placeholder="ABC-1234">
        <small class="text-muted">Formato: 3 letras mayúsculas, guión, 4 números</small>
    </div>

    <!-- Validación de email -->
    <div class="mb-3">
        <label for="email" class="form-label">Email de Contacto</label>
        <input type="email" 
               class="form-control" 
               id="email" 
               name="email"
               required>
    </div>

    <!-- Validación numérica con rango -->
    <div class="mb-3">
        <label for="precio" class="form-label">Precio</label>
        <input type="number" 
               class="form-control" 
               id="precio" 
               name="precio"
               min="0"
               max="9999.99"
               step="0.01"
               required>
    </div>

    <!-- Validación de URL -->
    <div class="mb-3">
        <label for="url" class="form-label">URL del Producto</label>
        <input type="url" 
               class="form-control" 
               id="url" 
               name="url"
               placeholder="https://ejemplo.com/pastel">
    </div>

    <!-- Validación de fecha -->
    <div class="mb-3">
        <label for="fecha_vencimiento" class="form-label">Fecha de Vencimiento</label>
        <input type="date" 
               class="form-control" 
               id="fecha_vencimiento" 
               name="fecha_vencimiento"
               min="{{ date('Y-m-d') }}">
    </div>

    <button type="submit" class="btn btn-primary">Guardar Pastel</button>
</form>

Atributos de Validación HTML5

blade
<!-- Atributos clave de validación -->
<input 
    type="text"
    required                    <!-- Campo obligatorio -->
    minlength="3"               <!-- Longitud mínima -->
    maxlength="100"             <!-- Longitud máxima -->
    pattern="[A-Za-z\s]+"       <!-- Patrón con regex -->
    min="0"                     <!-- Valor mínimo (numérico) -->
    max="100"                   <!-- Valor máximo (numérico) -->
    step="0.01"                 <!-- Incremento permitido -->
    title="Solo letras"         <!-- Mensaje de ayuda en validación -->
    placeholder="Ingresa el nombre"
    autocomplete="off"
>

3. Validación con JavaScript y jQuery

Validación con jQuery Validate

El plugin jQuery Validate es la opción más popular para validación en frontend:

html
<!-- 1. Incluir dependencias -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/jquery.validate.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/additional-methods.min.js"></script>
blade
@extends('layouts.app')

@section('content')
<div class="row justify-content-center">
    <div class="col-md-8">
        <div class="card">
            <div class="card-header">
                <h5 class="mb-0">Crear Nuevo Pastel</h5>
            </div>
            <div class="card-body">
                <form id="pastelForm" action="{{ route('pasteles.store') }}" method="POST">
                    @csrf
                    
                    <div class="mb-3">
                        <label for="nombre" class="form-label">Nombre *</label>
                        <input type="text" 
                               class="form-control" 
                               id="nombre" 
                               name="nombre">
                        <div class="invalid-feedback"></div>
                    </div>

                    <div class="mb-3">
                        <label for="sabor" class="form-label">Sabor *</label>
                        <select class="form-select" id="sabor" name="sabor">
                            <option value="">Selecciona un sabor...</option>
                            <option value="chocolate">Chocolate</option>
                            <option value="vainilla">Vainilla</option>
                            <option value="cheesecake">Cheesecake</option>
                            <option value="fresa">Fresa</option>
                            <option value="coco">Coco</option>
                        </select>
                        <div class="invalid-feedback"></div>
                    </div>

                    <div class="mb-3">
                        <label for="precio" class="form-label">Precio</label>
                        <input type="number" 
                               class="form-control" 
                               id="precio" 
                               name="precio"
                               step="0.01">
                        <div class="invalid-feedback"></div>
                    </div>

                    <div class="mb-3">
                        <label for="email" class="form-label">Email de Contacto</label>
                        <input type="email" 
                               class="form-control" 
                               id="email" 
                               name="email">
                        <div class="invalid-feedback"></div>
                    </div>

                    <div class="mb-3">
                        <label for="descripcion" class="form-label">Descripción</label>
                        <textarea class="form-control" 
                                  id="descripcion" 
                                  name="descripcion" 
                                  rows="3"></textarea>
                        <div class="invalid-feedback"></div>
                    </div>

                    <button type="submit" class="btn btn-primary">
                        <i class="fas fa-save"></i> Guardar Pastel
                    </button>
                </form>
            </div>
        </div>
    </div>
</div>
@endsection

@push('scripts')
<script>
$(document).ready(function() {
    // Extender jQuery Validate con validación personalizada
    $.validator.addMethod("saborValido", function(value, element) {
        return this.optional(element) || 
               ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'].includes(value);
    }, "Por favor selecciona un sabor válido.");

    $.validator.addMethod("precioValido", function(value, element) {
        return this.optional(element) || 
               (parseFloat(value) >= 0 && parseFloat(value) <= 9999.99);
    }, "El precio debe estar entre 0 y 9999.99.");

    // Configuración de validación del formulario
    $("#pastelForm").validate({
        // Configuración general
        errorClass: 'is-invalid',
        validClass: 'is-valid',
        errorElement: 'div',
        
        // Reglas de validación
        rules: {
            nombre: {
                required: true,
                minlength: 3,
                maxlength: 100,
                regex: /^[a-zA-ZáéíóúñÑ\s]+$/ // Solo letras y espacios
            },
            sabor: {
                required: true,
                saborValido: true
            },
            precio: {
                number: true,
                precioValido: true
            },
            email: {
                email: true
            },
            descripcion: {
                maxlength: 1000
            }
        },

        // Mensajes de error personalizados
        messages: {
            nombre: {
                required: "El nombre del pastel es obligatorio.",
                minlength: "El nombre debe tener al menos 3 caracteres.",
                maxlength: "El nombre no puede exceder los 100 caracteres.",
                regex: "El nombre solo puede contener letras y espacios."
            },
            sabor: {
                required: "Debes seleccionar un sabor."
            },
            precio: {
                number: "Ingresa un precio válido."
            },
            email: {
                email: "Ingresa un email válido."
            },
            descripcion: {
                maxlength: "La descripción no puede exceder los 1000 caracteres."
            }
        },

        // Manejo de errores
        errorPlacement: function(error, element) {
            error.addClass('text-danger small mt-1');
            error.insertAfter(element);
        },

        // Highlighting de campos
        highlight: function(element) {
            $(element).removeClass('is-valid').addClass('is-invalid');
        },
        unhighlight: function(element) {
            $(element).removeClass('is-invalid').addClass('is-valid');
        },

        // Validación en tiempo real
        onkeyup: function(element) {
            $(element).valid();
        },
        onfocusout: function(element) {
            $(element).valid();
        },

        // Antes de enviar el formulario
        submitHandler: function(form) {
            // Mostrar spinner
            $('button[type="submit"]').html(
                '<span class="spinner-border spinner-border-sm" role="status"></span> Guardando...'
            ).prop('disabled', true);
            
            form.submit();
        },

        // Si hay errores
        invalidHandler: function(event, validator) {
            // Scroll al primer campo con error
            var firstError = validator.errorList[0].element;
            $('html, body').animate({
                scrollTop: $(firstError).offset().top - 100
            }, 500);
            $(firstError).focus();
        }
    });

    // Validación de campo específico en tiempo real
    $('#nombre').on('input', function() {
        if ($(this).val().length > 0) {
            $(this).valid();
        }
    });
});
</script>
@endpush

Validación con Alpine.js (Recomendado para Laravel)

Alpine.js es la opción moderna para Laravel:

blade
<div x-data="{
    form: {
        nombre: '',
        sabor: '',
        precio: '',
        email: '',
        descripcion: ''
    },
    errors: {},
    
    validate() {
        this.errors = {};
        
        // Validar nombre
        if (!this.form.nombre || this.form.nombre.length < 3) {
            this.errors.nombre = 'El nombre debe tener al menos 3 caracteres';
        }
        
        // Validar sabor
        if (!this.form.sabor) {
            this.errors.sabor = 'Debes seleccionar un sabor';
        }
        
        // Validar email
        if (this.form.email && !this.isValidEmail(this.form.email)) {
            this.errors.email = 'Ingresa un email válido';
        }
        
        // Validar precio
        if (this.form.precio && (this.form.precio < 0 || this.form.precio > 9999.99)) {
            this.errors.precio = 'El precio debe estar entre 0 y 9999.99';
        }
        
        return Object.keys(this.errors).length === 0;
    },
    
    isValidEmail(email) {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    },
    
    submitForm() {
        if (this.validate()) {
            // Enviar formulario
            document.getElementById('pastelForm').submit();
        }
    }
}" 
x-init="() => {
    // Validación en tiempo real
    $watch('form.nombre', () => validate());
}">
    
    <form id="pastelForm" action="{{ route('pasteles.store') }}" method="POST">
        @csrf
        
        <div class="mb-3">
            <label class="form-label">Nombre *</label>
            <input type="text" 
                   class="form-control" 
                   x-model="form.nombre"
                   :class="{ 'is-invalid': errors.nombre }">
            <div x-show="errors.nombre" class="text-danger small" x-text="errors.nombre"></div>
        </div>
        
        <!-- ... resto del formulario ... -->
        
        <button type="button" class="btn btn-primary" @click="submitForm()">
            Guardar Pastel
        </button>
    </form>
</div>

4. Validación en el Servidor con Form Requests

Los Form Requests son la forma más elegante de validar en Laravel 12.

Crear Form Request

bash
# Crear request para store
php artisan make:request StorePastelRequest

# Crear request para update
php artisan make:request UpdatePastelRequest

StorePastelRequest

php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StorePastelRequest extends FormRequest
{
    /**
     * Determina si el usuario está autorizado
     */
    public function authorize(): bool
    {
        // Implementar lógica de autorización
        // Ejemplo: return auth()->user()->can('crear-pasteles');
        return true;
    }

    /**
     * Prepara los datos antes de la validación
     */
    protected function prepareForValidation(): void
    {
        // Sanitizar datos
        $this->merge([
            'nombre' => trim($this->nombre),
            'sabor' => strtolower($this->sabor),
            'disponible' => $this->has('disponible'),
        ]);
    }

    /**
     * Reglas de validación
     */
    public function rules(): array
    {
        return [
            'nombre' => [
                'required',
                'string',
                'min:3',
                'max:100',
                'regex:/^[a-zA-ZáéíóúñÑ\s]+$/',
                Rule::unique('pasteles', 'nombre')
            ],
            'sabor' => [
                'required',
                Rule::in(['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'])
            ],
            'precio' => [
                'nullable',
                'numeric',
                'min:0',
                'max:9999.99'
            ],
            'email' => [
                'nullable',
                'email',
                'max:255'
            ],
            'descripcion' => [
                'nullable',
                'string',
                'max:1000'
            ],
            'disponible' => [
                'boolean'
            ],
            'fecha_vencimiento' => [
                'nullable',
                'date',
                'after:today'
            ]
        ];
    }

    /**
     * Mensajes de error personalizados
     */
    public function messages(): array
    {
        return [
            'nombre.required' => 'El nombre del pastel es obligatorio.',
            'nombre.min' => 'El nombre debe tener al menos :min caracteres.',
            'nombre.max' => 'El nombre no puede exceder los :max caracteres.',
            'nombre.unique' => 'Ya existe un pastel con este nombre.',
            'nombre.regex' => 'El nombre solo puede contener letras y espacios.',
            'sabor.required' => 'Debes seleccionar un sabor.',
            'sabor.in' => 'El sabor seleccionado no es válido.',
            'precio.numeric' => 'El precio debe ser un número.',
            'precio.min' => 'El precio no puede ser negativo.',
            'precio.max' => 'El precio no puede exceder :max.',
            'email.email' => 'Ingresa un email válido.',
            'email.max' => 'El email no puede exceder los :max caracteres.',
            'descripcion.max' => 'La descripción no puede exceder los :max caracteres.',
            'fecha_vencimiento.after' => 'La fecha de vencimiento debe ser posterior a hoy.'
        ];
    }

    /**
     * Atributos personalizados para mensajes
     */
    public function attributes(): array
    {
        return [
            'nombre' => 'nombre del pastel',
            'sabor' => 'sabor',
            'precio' => 'precio',
            'email' => 'email de contacto',
            'descripcion' => 'descripción',
            'disponible' => 'disponibilidad'
        ];
    }

    /**
     * Validación después de pasar las reglas
     */
    public function withValidator($validator): void
    {
        $validator->after(function ($validator) {
            // Validación personalizada adicional
            if ($this->precio && $this->precio > 1000 && $this->sabor === 'cheesecake') {
                $validator->errors()->add(
                    'precio',
                    'Los pasteles de cheesecake no pueden costar más de $1,000.'
                );
            }
        });
    }

    /**
     * Obtener datos validados con transformaciones
     */
    public function validated($key = null, $default = null)
    {
        $validated = parent::validated();
        
        // Añadir campos adicionales
        $validated['nombre'] = ucwords(strtolower($validated['nombre']));
        $validated['disponible'] = $this->has('disponible');
        
        return $validated;
    }
}

UpdatePastelRequest

php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

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

    public function rules(): array
    {
        // Obtener el ID del pastel desde la ruta
        $pastelId = $this->route('pastel')->id ?? $this->route('pastel');
        
        return [
            'nombre' => [
                'required',
                'string',
                'min:3',
                'max:100',
                Rule::unique('pasteles')->ignore($pastelId)
            ],
            'sabor' => [
                'required',
                Rule::in(['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'])
            ],
            'precio' => 'nullable|numeric|min:0|max:9999.99',
            'email' => 'nullable|email|max:255',
            'descripcion' => 'nullable|string|max:1000',
            'disponible' => 'boolean'
        ];
    }

    public function messages(): array
    {
        return [
            'nombre.required' => 'El nombre del pastel es obligatorio.',
            'nombre.unique' => 'Ya existe un pastel con este nombre.',
            'sabor.required' => 'Debes seleccionar un sabor.',
            'sabor.in' => 'El sabor seleccionado no es válido.'
        ];
    }
}

Usar Form Request en el Controlador

php
<?php

namespace App\Http\Controllers;

use App\Models\Pastel;
use App\Http\Requests\StorePastelRequest;
use App\Http\Requests\UpdatePastelRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

class PastelController extends Controller
{
    public function store(StorePastelRequest $request): RedirectResponse
    {
        // Los datos ya están validados
        // $request->validated() devuelve los datos validados y transformados
        $pastel = Pastel::create($request->validated());
        
        return redirect()
            ->route('pasteles.index')
            ->with('success', '¡Pastel creado exitosamente!');
    }

    public function update(UpdatePastelRequest $request, Pastel $pastel): RedirectResponse
    {
        $pastel->update($request->validated());
        
        return redirect()
            ->route('pasteles.index')
            ->with('success', '¡Pastel actualizado exitosamente!');
    }
}

5. Validación Directa en Controladores

Para casos simples, puedes validar directamente en el controlador:

php
<?php

namespace App\Http\Controllers;

use App\Models\Pastel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class PastelController extends Controller
{
    public function store(Request $request)
    {
        // Validación directa
        $validator = Validator::make($request->all(), [
            'nombre' => 'required|string|max:100|unique:pasteles',
            'sabor' => 'required|in:chocolate,vainilla,cheesecake',
            'precio' => 'nullable|numeric|min:0'
        ], [
            'nombre.required' => 'El nombre es obligatorio',
            'nombre.unique' => 'Este nombre ya existe'
        ]);

        if ($validator->fails()) {
            return redirect()
                ->back()
                ->withErrors($validator)
                ->withInput();
        }

        $pastel = Pastel::create($validator->validated());
        return redirect()->route('pasteles.index');
    }

    // Alternativa con validate()
    public function update(Request $request, Pastel $pastel)
    {
        $validated = $request->validate([
            'nombre' => 'required|string|max:100|unique:pasteles,nombre,' . $pastel->id,
            'sabor' => 'required|in:chocolate,vainilla,cheesecake',
            'precio' => 'nullable|numeric|min:0'
        ]);

        $pastel->update($validated);
        return redirect()->route('pasteles.index');
    }
}

6. Mensajes de Error Personalizados

En el Form Request

php
public function messages(): array
{
    return [
        'nombre.required' => 'El :attribute es obligatorio.',
        'nombre.unique' => 'El :attribute ya está en uso.',
        'sabor.required' => 'Selecciona un :attribute válido.',
    ];
}

public function attributes(): array
{
    return [
        'nombre' => 'nombre del pastel',
        'sabor' => 'sabor',
        'precio' => 'precio',
    ];
}

Archivo de Idioma

Crear resources/lang/es/validation.php:

php
<?php

return [
    'accepted' => 'El campo :attribute debe ser aceptado.',
    'active_url' => 'El campo :attribute no es una URL válida.',
    'after' => 'El campo :attribute debe ser una fecha posterior a :date.',
    'alpha' => 'El campo :attribute solo debe contener letras.',
    'alpha_dash' => 'El campo :attribute solo debe contener letras, números, guiones y guiones bajos.',
    'alpha_num' => 'El campo :attribute solo debe contener letras y números.',
    'array' => 'El campo :attribute debe ser un conjunto.',
    'before' => 'El campo :attribute debe ser una fecha anterior a :date.',
    'between' => [
        'numeric' => 'El campo :attribute debe estar entre :min y :max.',
        'file' => 'El peso del campo :attribute debe estar entre :min y :max kilobytes.',
        'string' => 'El campo :attribute debe tener entre :min y :max caracteres.',
        'array' => 'El campo :attribute debe tener entre :min y :max elementos.',
    ],
    'boolean' => 'El campo :attribute debe ser verdadero o falso.',
    'confirmed' => 'La confirmación de :attribute no coincide.',
    'date' => 'El campo :attribute no es una fecha válida.',
    'date_format' => 'El campo :attribute no corresponde con el formato :format.',
    'different' => 'Los campos :attribute y :other deben ser diferentes.',
    'digits' => 'El campo :attribute debe tener :digits dígitos.',
    'digits_between' => 'El campo :attribute debe tener entre :min y :max dígitos.',
    'dimensions' => 'El campo :attribute tiene dimensiones de imagen no válidas.',
    'distinct' => 'El campo :attribute tiene un valor duplicado.',
    'email' => 'El campo :attribute no es un correo válido.',
    'exists' => 'El campo :attribute seleccionado no es válido.',
    'file' => 'El campo :attribute debe ser un archivo.',
    'filled' => 'El campo :attribute es obligatorio.',
    'gt' => [
        'numeric' => 'El campo :attribute debe ser mayor que :value.',
        'file' => 'El peso del campo :attribute debe ser mayor que :value kilobytes.',
        'string' => 'El campo :attribute debe tener más de :value caracteres.',
        'array' => 'El campo :attribute debe tener más de :value elementos.',
    ],
    'gte' => [
        'numeric' => 'El campo :attribute debe ser mayor o igual que :value.',
        'file' => 'El peso del campo :attribute debe ser mayor o igual que :value kilobytes.',
        'string' => 'El campo :attribute debe tener :value caracteres o más.',
        'array' => 'El campo :attribute debe tener :value elementos o más.',
    ],
    'image' => 'El campo :attribute debe ser una imagen.',
    'in' => 'El campo :attribute seleccionado no es válido.',
    'in_array' => 'El campo :attribute debe existir en :other.',
    'integer' => 'El campo :attribute debe ser un número entero.',
    'ip' => 'El campo :attribute debe ser una dirección IP válida.',
    'ipv4' => 'El campo :attribute debe ser una dirección IPv4 válida.',
    'ipv6' => 'El campo :attribute debe ser una dirección IPv6 válida.',
    'json' => 'El campo :attribute debe ser una cadena JSON válida.',
    'lt' => [
        'numeric' => 'El campo :attribute debe ser menor que :value.',
        'file' => 'El peso del campo :attribute debe ser menor que :value kilobytes.',
        'string' => 'El campo :attribute debe tener menos de :value caracteres.',
        'array' => 'El campo :attribute debe tener menos de :value elementos.',
    ],
    'lte' => [
        'numeric' => 'El campo :attribute debe ser menor o igual que :value.',
        'file' => 'El peso del campo :attribute debe ser menor o igual que :value kilobytes.',
        'string' => 'El campo :attribute debe tener :value caracteres o menos.',
        'array' => 'El campo :attribute debe tener :value elementos o menos.',
    ],
    'max' => [
        'numeric' => 'El campo :attribute no debe ser mayor que :max.',
        'file' => 'El peso del campo :attribute no debe ser mayor que :max kilobytes.',
        'string' => 'El campo :attribute no debe tener más de :max caracteres.',
        'array' => 'El campo :attribute no debe tener más de :max elementos.',
    ],
    'mimes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
    'mimetypes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
    'min' => [
        'numeric' => 'El campo :attribute debe ser como mínimo :min.',
        'file' => 'El peso del campo :attribute debe ser como mínimo :min kilobytes.',
        'string' => 'El campo :attribute debe tener como mínimo :min caracteres.',
        'array' => 'El campo :attribute debe tener como mínimo :min elementos.',
    ],
    'not_in' => 'El campo :attribute seleccionado no es válido.',
    'not_regex' => 'El formato del campo :attribute no es válido.',
    'numeric' => 'El campo :attribute debe ser un número.',
    'present' => 'El campo :attribute debe estar presente.',
    'regex' => 'El formato del campo :attribute no es válido.',
    'required' => 'El campo :attribute es obligatorio.',
    'required_if' => 'El campo :attribute es obligatorio cuando :other es :value.',
    'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.',
    'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.',
    'required_with_all' => 'El campo :attribute es obligatorio cuando :values están presentes.',
    'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.',
    'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values están presentes.',
    'same' => 'Los campos :attribute y :other deben coincidir.',
    'size' => [
        'numeric' => 'El campo :attribute debe ser :size.',
        'file' => 'El peso del campo :attribute debe ser :size kilobytes.',
        'string' => 'El campo :attribute debe tener :size caracteres.',
        'array' => 'El campo :attribute debe contener :size elementos.',
    ],
    'string' => 'El campo :attribute debe ser una cadena de caracteres.',
    'timezone' => 'El campo :attribute debe ser una zona horaria válida.',
    'unique' => 'El campo :attribute ya ha sido registrado.',
    'uploaded' => 'El campo :attribute no se pudo subir correctamente.',
    'url' => 'El formato del campo :attribute no es válido.',
    'uuid' => 'El campo :attribute debe ser un UUID válido.',
];

7. Validaciones Personalizadas

Crear Regla Personalizada

bash
php artisan make:rule PrecioMinimo
php
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;

class PrecioMinimo implements ValidationRule
{
    public function __construct(
        private int $minimo = 0,
        private ?string $sabor = null
    ) {}

    /**
     * Run the validation rule.
     *
     * @param  Closure(string): PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Regla con lógica personalizada
        if ($this->sabor === 'cheesecake' && $value < 200) {
            $fail('Los pasteles de cheesecake deben tener un precio mínimo de $200.');
        }
        
        if ($value < $this->minimo) {
            $fail("El precio mínimo para este pastel es de $this->minimo.");
        }
    }
}

Usar la Regla Personalizada

php
use App\Rules\PrecioMinimo;

// En el Form Request
public function rules(): array
{
    return [
        'precio' => [
            'required',
            'numeric',
            new PrecioMinimo(100, $this->sabor)
        ]
    ];
}

// En el controlador
$request->validate([
    'precio' => [
        'required',
        'numeric',
        new PrecioMinimo(100, $request->sabor)
    ]
]);

Validación Personalizada con Closures

php
public function rules(): array
{
    return [
        'nombre' => [
            'required',
            'string',
            function (string $attribute, mixed $value, Closure $fail) {
                if (str_contains($value, 'prohibido')) {
                    $fail('El nombre no puede contener la palabra "prohibido".');
                }
            }
        ]
    ];
}

Extender el Validador

php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;

class ValidatorServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Validator::extend('sin_profanidad', function ($attribute, $value, $parameters, $validator) {
            $profanidades = ['palabra1', 'palabra2', 'palabra3'];
            return !str_contains($value, $profanidades);
        });

        Validator::extend('mayuscula_inicial', function ($attribute, $value, $parameters, $validator) {
            return ucfirst($value) === $value;
        });

        Validator::replacer('sin_profanidad', function ($message, $attribute, $rule, $parameters) {
            return str_replace(':attribute', $attribute, 'El :attribute contiene palabras inapropiadas.');
        });
    }
}

8. Validación en Tiempo Real

Validación Asíncrona con AJAX

javascript
// Validación en tiempo real con AJAX
$(document).ready(function() {
    $('#nombre').on('blur', function() {
        const nombre = $(this).val();
        const $element = $(this);
        
        if (nombre.length < 3) {
            showError($element, 'El nombre debe tener al menos 3 caracteres');
            return;
        }

        $.ajax({
            url: '/api/validar-nombre',
            method: 'POST',
            data: { nombre: nombre },
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            },
            success: function(response) {
                if (response.valid) {
                    $element.removeClass('is-invalid').addClass('is-valid');
                    $element.next('.invalid-feedback').remove();
                } else {
                    showError($element, response.message);
                }
            },
            error: function() {
                showError($element, 'Error al validar el nombre');
            }
        });
    });

    function showError(element, message) {
        element.removeClass('is-valid').addClass('is-invalid');
        if (element.next('.invalid-feedback').length === 0) {
            element.after(`<div class="invalid-feedback">${message}</div>`);
        } else {
            element.next('.invalid-feedback').text(message);
        }
    }
});

Ruta para Validación AJAX

php
// routes/api.php
use Illuminate\Http\Request;

Route::post('/validar-nombre', function (Request $request) {
    $nombre = $request->input('nombre');
    
    // Validar en base de datos
    $existe = Pastel::where('nombre', $nombre)->exists();
    
    return response()->json([
        'valid' => !$existe,
        'message' => $existe ? 'Este nombre ya está registrado.' : 'Nombre disponible'
    ]);
});

9. Buenas Prácticas

1. Usar Form Requests para Validaciones Complejas

php
// ✅ Recomendado para validaciones complejas
public function store(StorePastelRequest $request)
{
    // Validación automática
}

// ❌ Evitar para validaciones complejas
public function store(Request $request)
{
    $request->validate([...]); // Mejor usar Form Request
}

2. Sanitizar Datos

php
// En el Form Request
protected function prepareForValidation(): void
{
    $this->merge([
        'nombre' => trim($this->nombre),
        'email' => strtolower($this->email),
    ]);
}

3. Usar Reglas de Validación Específicas

php
// ✅ Específico y claro
'email' => 'required|email|unique:users'

// ❌ Genérico
'email' => 'required|string'

4. Agrupar Validaciones

php
// Usar arrays en lugar de pipes para reglas complejas
'nombre' => [
    'required',
    'string',
    'min:3',
    'max:100',
    Rule::unique('pasteles')
]

5. Manejo de Errores en Vistas

blade
<!-- Mostrar todos los errores -->
@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

<!-- Mostrar error por campo -->
<input type="text" class="form-control @error('nombre') is-invalid @enderror" 
       name="nombre" value="{{ old('nombre') }}">
@error('nombre')
    <div class="invalid-feedback">{{ $message }}</div>
@enderror

6. Validación con Relaciones

php
// Validar existencia en otra tabla
'categoria_id' => 'required|exists:categorias,id'

// Validar unicidad con condición
'email' => Rule::unique('users')->where(function ($query) {
    return $query->where('estado', 'activo');
})

7. Validación Condicional

php
public function rules(): array
{
    $rules = [
        'nombre' => 'required|string',
        'sabor' => 'required|string',
    ];

    // Validación condicional
    if ($this->input('tipo') === 'especial') {
        $rules['precio'] = 'required|numeric|min:100';
    }

    return $rules;
}

Resumen

MétodoVentajasCuándo Usar
HTML5Rápido, sin JSValidaciones básicas
jQuery ValidateExperiencia de usuario mejoradaFormularios complejos
Form RequestCódigo limpio, reutilizableProyectos grandes
ControladorSimple, directoCasos simples
Validación PersonalizadaFlexibilidad totalReglas de negocio específicas

Comandos Útiles

bash
# Crear Form Request
php artisan make:request NombreRequest

# Crear regla personalizada
php artisan make:rule NombreRegla

# Crear proveedor de validación
php artisan make:provider ValidatorServiceProvider

# Ver reglas de validación disponibles
php artisan help make:request

La implementación de validaciones robustas en Laravel 12 es fundamental para construir aplicaciones seguras y confiables. Combina validación en cliente (HTML5 + JavaScript) con validación en servidor (Form Requests) para obtener la mejor experiencia de usuario posible sin comprometer la seguridad.

Comentarios

Entradas más populares de este blog

Tutorial CRUD con Laravel 12-Pastel::factory(50)->create();

CRUD Completo con Laravel 12 - Guía Práctica Paso a Paso-productos

CRUD en Laravel 12 con Git - Guía para Principiantes