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
Tipos de Validación
Validación en el Cliente
Validación con JavaScript y jQuery
Validación en el Servidor con Form Requests
Validación Directa en Controladores
Mensajes de Error Personalizados
Validaciones Personalizadas
Validación en Tiempo Real
Buenas Prácticas
1. Tipos de Validación
En una aplicación Laravel, podemos implementar validaciones en diferentes capas:
| Capa | Tecnología | Propósito |
|---|---|---|
| Cliente | HTML5, JavaScript | Experiencia de usuario inmediata |
| Cliente Avanzado | jQuery, Plugins | Validaciones más complejas en frontend |
| Servidor | Laravel Request | Seguridad e integridad de datos |
| Base de Datos | Migraciones, Modelos | Integridad 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:
<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
<!-- 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:
<!-- 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>@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>
@endpushValidación con Alpine.js (Recomendado para Laravel)
Alpine.js es la opción moderna para Laravel:
<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
# Crear request para store
php artisan make:request StorePastelRequest
# Crear request para update
php artisan make:request UpdatePastelRequestStorePastelRequest
<?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
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
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
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
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
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
php artisan make:rule PrecioMinimo<?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
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
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
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
// 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
// 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
// ✅ 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
// 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
// ✅ Específico y claro
'email' => 'required|email|unique:users'
// ❌ Genérico
'email' => 'required|string'4. Agrupar Validaciones
// 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
<!-- 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>
@enderror6. Validación con Relaciones
// 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
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étodo | Ventajas | Cuándo Usar |
|---|---|---|
| HTML5 | Rápido, sin JS | Validaciones básicas |
| jQuery Validate | Experiencia de usuario mejorada | Formularios complejos |
| Form Request | Código limpio, reutilizable | Proyectos grandes |
| Controlador | Simple, directo | Casos simples |
| Validación Personalizada | Flexibilidad total | Reglas de negocio específicas |
Comandos Útiles
# 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:requestLa 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
Publicar un comentario