laravelvalidacionphpformularios

Validación de Datos Complejos en Laravel: Guía Avanzada

Validación de Datos Complejos en Laravel: Guía Avanzada

La validación de datos es uno de los pilares fundamentales en cualquier aplicación web profesional. Mientras que muchos desarrolladores Laravel utilizan validación básica en sus controllers, existen técnicas avanzadas que transforman tu forma de manejar datos complejos y garantizan aplicaciones más robustas y mantenibles.

En esta guía, exploraremos estrategias avanzadas de validación que van más allá del $this->validate() básico, incluyendo validación anidada, reglas personalizadas, validación condicional sofisticada y patrones que escalan en proyectos reales.

Por qué la Validación Avanzada Importa

Antes de sumergirse en código, es importante entender por qué esta materia es crítica:

  • Seguridad: Validar datos al lado del servidor previene inyecciones y manipulaciones
  • UX mejorada: Mensajes de error personalizados guían mejor a los usuarios
  • Mantenibilidad: Código validador bien estructurado es más fácil de mantener y reutilizar
  • Rendimiento: La validación eficiente evita procesamiento innecesario

Recientemente, la comunidad Laravel ha reportado problemas de rendimiento con validación de wildcard en datasets grandes (como se vio en casos donde validaciones O(n²) tardaban 3+ segundos). Entender cómo optimizar es esencial.

Form Requests: El Patrón Principal

Los Form Requests son la columna vertebral de la validación en Laravel. Son clases dedicadas que centralizan toda la lógica de validación, alejándola de tus controllers.

Creando tu Primer Form Request

php artisan make:request StoreProductRequest

Esto genera un archivo en app/Http/Requests/StoreProductRequest.php:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        // Aquí verificas si el usuario tiene permiso
        return true; // Cambiar según tu lógica de negocio
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'price' => 'required|numeric|min:0.01',
            'description' => 'nullable|string|max:1000',
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => 'El nombre del producto es obligatorio',
            'price.numeric' => 'El precio debe ser un número válido',
            'price.min' => 'El precio no puede ser menor a 0.01',
        ];
    }
}

Ahora en tu controller:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreProductRequest;
use App\Models\Product;

class ProductController extends Controller
{
    public function store(StoreProductRequest $request)
    {
        // Los datos ya están validados
        $product = Product::create($request->validated());
        
        return redirect()->route('products.show', $product);
    }
}

El método validated() devuelve solo los datos que pasaron la validación, protegiéndote contra asignación masiva accidental.

Validación de Datos Anidados

Uno de los escenarios más complejos es validar estructuras JSON anidadas. Imagina un formulario para crear un pedido con múltiples artículos:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'customer_email' => 'required|email',
            'items' => 'required|array|min:1',
            'items.*.product_id' => 'required|exists:products,id',
            'items.*.quantity' => 'required|integer|min:1|max:999',
            'items.*.price' => 'required|numeric|min:0.01',
            'shipping' => 'required|array',
            'shipping.address' => 'required|string|max:255',
            'shipping.city' => 'required|string|max:100',
            'shipping.postal_code' => 'required|string|regex:/^\d{5}(-\d{4})?$/',
            'billing' => 'required|array',
            'billing.same_as_shipping' => 'boolean',
        ];
    }

    public function messages(): array
    {
        return [
            'items.required' => 'Debe incluir al menos un artículo',
            'items.*.product_id.exists' => 'El producto especificado no existe',
            'items.*.quantity.max' => 'No puede ordenar más de 999 unidades por artículo',
            'shipping.postal_code.regex' => 'Código postal inválido',
        ];
    }
}

En tu controller:

public function store(StoreOrderRequest $request)
{
    $data = $request->validated();
    
    // Los datos anidados están listos para procesar
    $order = Order::create([
        'customer_email' => $data['customer_email'],
        'shipping_address' => $data['shipping']['address'],
        'shipping_city' => $data['shipping']['city'],
    ]);

    // Crear items anidados
    foreach ($data['items'] as $item) {
        $order->items()->create($item);
    }

    return response()->json(['order_id' => $order->id], 201);
}

Reglas de Validación Personalizadas

Laravel incluye muchas reglas built-in, pero a menudo necesitas lógica específica de tu negocio. Aquí hay varias formas de crear reglas personalizadas:

Usar Closures para Reglas Inline

public function rules(): array
{
    return [
        'username' => [
            'required',
            'string',
            'min:3',
            function ($attribute, $value, $fail) {
                // Lógica personalizada
                if (strpos($value, 'admin') !== false) {
                    $fail('El nombre de usuario no puede contener "admin"');
                }
                
                // Validar contra la BD
                if (User::where('username', $value)->exists()) {
                    $fail('Este nombre de usuario ya está registrado');
                }
            },
        ],
    ];
}

Crear una Regla Personalizada Reutilizable

php artisan make:rule ValidPhoneNumber
<?php

namespace App\Rules;

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

class ValidPhoneNumber implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Limpiar el número de caracteres especiales
        $cleaned = preg_replace('/\D/', '', $value);
        
        if (strlen($cleaned) < 10 || strlen($cleaned) > 15) {
            $fail('El número de teléfono debe tener entre 10 y 15 dígitos');
        }
    }
}

Uso en Form Request:

use App\Rules\ValidPhoneNumber;

public function rules(): array
{
    return [
        'phone' => ['required', new ValidPhoneNumber()],
    ];
}

Validación Condicional Avanzada

Los escenarios reales a menudo requieren que una regla se aplique solo bajo ciertas condiciones:

public function rules(): array
{
    return [
        'account_type' => 'required|in:personal,business',
        'name' => 'required|string',
        // Si es cuenta business, requiere nombre comercial
        'business_name' => $this->when(
            $this->input('account_type') === 'business',
            'required|string',
            'nullable'
        ),
        // Empresa requiere documento fiscal
        'tax_id' => $this->when(
            $this->input('account_type') === 'business',
            'required|regex:/^\d{8}[A-Z]$/',
            'nullable'
        ),
        // Dirección requerida solo si no está en ciertos países
        'address' => $this->unless(
            in_array($this->input('country'), ['US', 'CA']),
            'required|string'
        ),
    ];
}

Alternativa más legible con método personalizado:

public function rules(): array
{
    $isBusinessAccount = $this->input('account_type') === 'business';
    
    return [
        'account_type' => 'required|in:personal,business',
        'name' => 'required|string',
        'business_name' => $isBusinessAccount ? 'required' : 'nullable',
        'tax_id' => $isBusinessAccount ? 'required|regex:/^\d{8}[A-Z]$/' : 'nullable',
    ];
}

Validación Asincrónica en Tiempo Real

Para mejorar la experiencia del usuario, valida ciertos campos en tiempo real con AJAX:

// routes/api.php
Route::post('/validate-username', function (Request $request) {
    $request->validate([
        'username' => 'required|min:3|max:20|alpha_dash',
    ]);

    $exists = User::where('username', $request->username)->exists();

    return response()->json([
        'available' => !$exists,
        'message' => $exists ? 'Este nombre ya está en uso' : 'Disponible',
    ]);
});

Desde el frontend (con Fetch API):

const usernameInput = document.getElementById('username');

usernameInput.addEventListener('blur', async () => {
    const response = await fetch('/api/validate-username', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
        },
        body: JSON.stringify({ username: usernameInput.value }),
    });

    const data = await response.json();
    const errorEl = document.getElementById('username-error');
    
    if (!data.available) {
        errorEl.textContent = data.message;
        errorEl.style.display = 'block';
    } else {
        errorEl.style.display = 'none';
    }
});

Manejo de Errores de Validación

Los errores de validación en Laravel se manejan automáticamente, pero entender cómo personalizarlos es valioso:

// En un Form Request
public function failedValidation(\Illuminate\Contracts\Validation\Validator $validator)
{
    throw new \Illuminate\Validation\ValidationException(
        $validator,
        response()->json([
            'success' => false,
            'errors' => $validator->errors(),
            'message' => 'Validación fallida'
        ], 422)
    );
}

Capturar errores en el controller (si es necesario):

try {
    $data = $request->validated();
} catch (\Illuminate\Validation\ValidationException $e) {
    return redirect()->back()
        ->withErrors($e->errors())
        ->withInput();
}

Optimización de Validación en Datasets Grandes

Como mencionamos anteriormente, validación wildcard en arrays grandes puede ser O(n²). Aquí hay estrategias de optimización:

1. Usar Validación en Batch

public function rules(): array
{
    return [
        'items' => 'array|max:1000', // Limitar tamaño del array
        'items.*.id' => 'required|integer',
        'items.*.quantity' => 'required|integer|min:1',
    ];
}

public function prepareForValidation(): void
{
    // Procesar en chunks si es muy grande
    if (count($this->items ?? []) > 500) {
        $this->merge([
            'items' => collect($this->items)
                ->chunk(100)
                ->flatten(1)
                ->toArray()
        ]);
    }
}

2. Validación Selectiva

public function rules(): array
{
    $rules = [
        'items' => 'array',
    ];

    // Solo validar campos que cambiaron
    if ($this->has('items')) {
        $rules['items.*.id'] = 'required|exists:products,id';
        $rules['items.*.quantity'] = 'required|integer|min:1';
    }

    return $rules;
}

Patrones de Validación Reutilizable

Crear una clase base para requests comunes:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

abstract class BaseFormRequest extends FormRequest
{
    public function authorize(): bool
    {
        return auth()->check();
    }

    protected function commonRules(): array
    {
        return [
            'created_at' => 'nullable|date',
            'updated_at' => 'nullable|date',
        ];
    }

    final public function rules(): array
    {
        return array_merge(
            $this->commonRules(),
            $this->customRules()
        );
    }

    abstract protected function customRules(): array;
}

Herencia:

class UpdateUserRequest extends BaseFormRequest
{
    protected function customRules(): array
    {
        return [
            'email' => 'required|email|unique:users,email,' . $this->user()->id,
            'name' => 'required|string|max:255',
        ];
    }
}

Puntos Clave

  • Form Requests son la forma profesional de centralizar validación en Laravel
  • La validación anidada con wildcard (items.*.field) maneja estructuras JSON complejas
  • Reglas personalizadas con ValidationRule escalan mejor que closures inline
  • Validación condicional permite lógica sofisticada basada en otros campos
  • Validación asincrónica mejora UX validando en tiempo real
  • Optimizar para datasets grandes limitando tamaños y usando validación selectiva
  • Reutilizar patrones con clases base y traits reduce duplicación
  • Usar validated() siempre para protegerse contra asignación masiva accidental