Validación de datos
Nunca confíes en los datos que envía el usuario. Laravel proporciona un sistema de validación potente y expresivo que te permite definir reglas claras para cada campo del formulario y mostrar mensajes de error útiles.
El método validate()
La forma más directa de validar datos en Laravel es usando el método
validate() del objeto Request. Este método recibe un
array con las reglas de validación para cada campo:
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'category_id' => 'required|exists:categories,id',
]);
$post = Post::create($validated);
return redirect()->route('posts.show', $post);
}
}
El método validate() hace dos cosas automáticamente:
- Si la validación pasa, devuelve un array con los datos validados
- Si falla, redirige al usuario de vuelta al formulario con los errores y los valores anteriores (old input)
El array $validated solo contiene los campos
que definiste en las reglas. Si el usuario envía campos
adicionales, serán ignorados. Esto es más seguro que usar
$request->all().
Reglas de validación comunes
Laravel incluye más de 90 reglas de validación. Estas son las más utilizadas:
<?php
$validated = $request->validate([
// Campos requeridos y tipos básicos
'name' => 'required|string|max:100',
'email' => 'required|email',
'age' => 'required|integer|min:18|max:120',
'price' => 'required|numeric|min:0',
// Campos opcionales
'phone' => 'nullable|string|max:20',
'bio' => 'nullable|string|max:500',
// Unicidad en base de datos
'username' => 'required|unique:users,username',
// Existencia en base de datos
'category_id' => 'required|exists:categories,id',
// Confirmación de campo (para password_confirmation)
'password' => 'required|string|min:8|confirmed',
// Fechas
'birth_date' => 'required|date|before:today',
'start_date' => 'required|date|after:today',
// Booleanos
'is_active' => 'boolean',
'accept_terms' => 'accepted',
// Arrays
'tags' => 'array|min:1|max:5',
'tags.*' => 'string|max:50',
]);
Sintaxis con arrays
Las reglas se pueden escribir como string separado por |
o como array. La sintaxis de array es útil cuando las reglas son
complejas o necesitas pasar parámetros con comas:
<?php
// Sintaxis con string (más concisa)
$validated = $request->validate([
'title' => 'required|string|max:255',
]);
// Sintaxis con array (más explícita)
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
]);
// Útil para reglas con parámetros complejos
$validated = $request->validate([
'status' => ['required', 'in:draft,published,archived'],
'role' => ['required', 'not_in:banned,suspended'],
]);
Validación en actualización (unique con excepción)
Al actualizar un registro, la regla unique debe excluir
el registro actual para no fallar cuando el usuario no cambia el valor:
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class UserController extends Controller
{
public function update(Request $request, User $user): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
// Excluir el usuario actual de la validación unique
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($user->id),
],
]);
$user->update($validated);
return redirect()->route('users.show', $user);
}
}
Mensajes de error personalizados
Por defecto, Laravel muestra mensajes en inglés. Puedes personalizar
los mensajes pasando un segundo argumento a validate():
<?php
$validated = $request->validate([
'title' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|min:8|confirmed',
], [
'title.required' => 'El título es obligatorio.',
'title.max' => 'El título no puede superar los 255 caracteres.',
'email.required' => 'El email es obligatorio.',
'email.email' => 'El email debe ser una dirección válida.',
'email.unique' => 'Este email ya está registrado.',
'password.required' => 'La contraseña es obligatoria.',
'password.min' => 'La contraseña debe tener al menos 8 caracteres.',
'password.confirmed' => 'Las contraseñas no coinciden.',
]);
Nombres de atributos personalizados
En lugar de personalizar cada mensaje, puedes cambiar el nombre del atributo que aparece en los mensajes automáticos usando el tercer argumento:
<?php
$validated = $request->validate(
[
'first_name' => 'required|string|max:100',
'last_name' => 'required|string|max:100',
'email' => 'required|email',
],
[], // Sin mensajes personalizados
[
'first_name' => 'nombre',
'last_name' => 'apellido',
'email' => 'correo electrónico',
]
);
// Ahora los mensajes dirán:
// "El campo nombre es obligatorio."
// en lugar de "The first name field is required."
Validación condicional
A veces necesitas aplicar reglas solo cuando se cumplen ciertas condiciones.
Usa sometimes o la regla required_if:
<?php
$validated = $request->validate([
'payment_method' => 'required|in:card,transfer,cash',
// card_number es obligatorio solo si payment_method es "card"
'card_number' => 'required_if:payment_method,card|nullable|string|size:16',
// bank_account es obligatorio solo si payment_method es "transfer"
'bank_account' => 'required_if:payment_method,transfer|nullable|string',
// company_name es obligatorio solo si is_company está marcado
'is_company' => 'boolean',
'company_name' => 'required_if:is_company,true|nullable|string|max:255',
]);
Mostrar errores en la vista
Como vimos en la lección anterior, los errores se muestran usando
la variable $errors y la directiva @error:
<form action="{{ route('posts.store') }}" method="POST">
@csrf
<div>
<label for="title">Título</label>
<input type="text" name="title" id="title"
value="{{ old('title') }}"
class="@error('title') is-invalid @enderror">
@error('title')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div>
<label for="email">Email</label>
<input type="email" name="email" id="email"
value="{{ old('email') }}"
class="@error('email') is-invalid @enderror">
@error('email')
<span class="error">{{ $message }}</span>
@enderror
</div>
<button type="submit">Guardar</button>
</form>
Ejemplo completo: registro de usuario
Veamos un ejemplo práctico con validación completa para el registro de usuarios:
<?php
// routes/web.php
use App\Http\Controllers\RegisterController;
Route::get('/register', [RegisterController::class, 'create'])->name('register');
Route::post('/register', [RegisterController::class, 'store']);
<?php
// app/Http/Controllers/RegisterController.php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\View\View;
class RegisterController extends Controller
{
public function create(): View
{
return view('auth.register');
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:8|confirmed',
], [
'name.required' => 'El nombre es obligatorio.',
'email.required' => 'El email es obligatorio.',
'email.email' => 'Introduce un email válido.',
'email.unique' => 'Este email ya está registrado.',
'password.required' => 'La contraseña es obligatoria.',
'password.min' => 'La contraseña debe tener al menos 8 caracteres.',
'password.confirmed' => 'Las contraseñas no coinciden.',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
return redirect()->route('login');
}
}
<!-- resources/views/auth/register.blade.php -->
@extends('layouts.app')
@section('content')
<h1>Crear cuenta</h1>
<form action="{{ route('register') }}" method="POST">
@csrf
<div>
<label for="name">Nombre</label>
<input type="text" name="name" id="name"
value="{{ old('name') }}"
class="@error('name') is-invalid @enderror">
@error('name')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div>
<label for="email">Email</label>
<input type="email" name="email" id="email"
value="{{ old('email') }}"
class="@error('email') is-invalid @enderror">
@error('email')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div>
<label for="password">Contraseña</label>
<input type="password" name="password" id="password"
class="@error('password') is-invalid @enderror">
@error('password')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div>
<label for="password_confirmation">Confirmar contraseña</label>
<input type="password" name="password_confirmation" id="password_confirmation">
</div>
<button type="submit">Registrarse</button>
</form>
@endsection
La regla confirmed busca automáticamente un campo
con el mismo nombre más _confirmation. Si el campo
se llama password, buscará password_confirmation.
Resumen
$request->validate()valida y devuelve los datos o redirige con errores- Las reglas se separan con
|o se pasan como array - Usa
Rule::unique()->ignore($id)para validar unique al actualizar - Personaliza mensajes con el segundo argumento de validate()
required_ifvalida condicionalmente según otro campo- La regla
confirmedbusca un campo_confirmation
Ejercicios
Ejercicio 1: Validar formulario de producto
Crea la validación para un formulario de producto con los siguientes campos:
name (obligatorio, máximo 200 caracteres),
price (obligatorio, numérico, mínimo 0.01),
stock (obligatorio, entero, mínimo 0),
category_id (obligatorio, debe existir en la tabla categories).
Incluye mensajes personalizados en español.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:200',
'price' => 'required|numeric|min:0.01',
'stock' => 'required|integer|min:0',
'category_id' => 'required|exists:categories,id',
], [
'name.required' => 'El nombre del producto es obligatorio.',
'name.max' => 'El nombre no puede superar los 200 caracteres.',
'price.required' => 'El precio es obligatorio.',
'price.numeric' => 'El precio debe ser un número.',
'price.min' => 'El precio debe ser mayor a 0.',
'stock.required' => 'El stock es obligatorio.',
'stock.integer' => 'El stock debe ser un número entero.',
'stock.min' => 'El stock no puede ser negativo.',
'category_id.required' => 'Selecciona una categoría.',
'category_id.exists' => 'La categoría seleccionada no existe.',
]);
$product = Product::create($validated);
return redirect()->route('products.show', $product);
}
}
Ejercicio 2: Validar actualización de perfil
Crea la validación para actualizar el perfil de un usuario.
El email debe ser único pero ignorando el usuario actual.
Campos: name (obligatorio, máximo 100),
email (obligatorio, email válido, único excepto el usuario actual),
bio (opcional, máximo 500 caracteres).
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class ProfileController extends Controller
{
public function update(Request $request, User $user): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($user->id),
],
'bio' => 'nullable|string|max:500',
], [
'name.required' => 'El nombre es obligatorio.',
'name.max' => 'El nombre no puede superar los 100 caracteres.',
'email.required' => 'El email es obligatorio.',
'email.email' => 'El email no es válido.',
'email.unique' => 'Este email ya está en uso.',
'bio.max' => 'La biografía no puede superar los 500 caracteres.',
]);
$user->update($validated);
return redirect()->route('profile.show', $user);
}
}
Ejercicio 3: Validación condicional de pago
Crea la validación para un formulario de pago donde:
payment_method es obligatorio y puede ser "card" o "transfer".
Si es "card", card_number es obligatorio (16 dígitos).
Si es "transfer", bank_account es obligatorio.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PaymentController extends Controller
{
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'payment_method' => 'required|in:card,transfer',
'card_number' => 'required_if:payment_method,card|nullable|digits:16',
'bank_account' => 'required_if:payment_method,transfer|nullable|string|max:30',
], [
'payment_method.required' => 'Selecciona un método de pago.',
'payment_method.in' => 'Método de pago no válido.',
'card_number.required_if' => 'El número de tarjeta es obligatorio.',
'card_number.digits' => 'El número de tarjeta debe tener 16 dígitos.',
'bank_account.required_if' => 'La cuenta bancaria es obligatoria.',
]);
// Procesar el pago...
return redirect()->route('payment.success');
}
}
¿Has encontrado un error o tienes una sugerencia para mejorar esta lección?
Escríbenos¿Te está gustando el curso?
Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.
Descubrir cursos premium