Form Requests
Cuando la validación en el controlador se vuelve compleja, los Form Requests son la solución. Son clases dedicadas que encapsulan la lógica de validación y autorización, manteniendo tus controladores limpios y tu código reutilizable.
¿Qué es un Form Request?
Un Form Request es una clase que extiende de
Illuminate\Foundation\Http\FormRequest. Contiene
dos métodos principales:
authorize(): determina si el usuario puede realizar la acciónrules(): define las reglas de validación
En la lección anterior vimos cómo validar con $request->validate().
Funciona bien para casos simples, pero cuando tienes muchas reglas,
mensajes personalizados o lógica de autorización, el controlador
se vuelve difícil de leer.
Crear un Form Request
Usa Artisan para crear un Form Request:
php artisan make:request StorePostRequest
Esto crea el archivo app/Http/Requests/StorePostRequest.php:
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'content' => 'required|string',
'category_id' => 'required|exists:categories,id',
];
}
}
La convención es nombrar los Form Requests según la acción:
StorePostRequest para crear,
UpdatePostRequest para actualizar.
Usar el Form Request en el controlador
En lugar de inyectar Request, inyecta tu Form Request.
Laravel ejecuta automáticamente la validación antes de que se ejecute
el método del controlador:
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
class PostController extends Controller
{
public function store(StorePostRequest $request): RedirectResponse
{
// Si llegamos aquí, la validación ya pasó
$post = Post::create($request->validated());
return redirect()->route('posts.show', $post);
}
}
El método validated() devuelve solo los datos que pasaron
la validación. Si la validación falla, Laravel redirige automáticamente
al formulario con los errores.
Mensajes personalizados
Añade el método messages() para personalizar los mensajes
de error:
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'content' => 'required|string',
'category_id' => 'required|exists:categories,id',
];
}
public function messages(): array
{
return [
'title.required' => 'El título es obligatorio.',
'title.max' => 'El título no puede superar los 255 caracteres.',
'content.required' => 'El contenido es obligatorio.',
'category_id.required' => 'Selecciona una categoría.',
'category_id.exists' => 'La categoría seleccionada no existe.',
];
}
}
Nombres de atributos personalizados
Usa el método attributes() para cambiar los nombres de los
campos en los mensajes automáticos:
<?php
public function attributes(): array
{
return [
'title' => 'título',
'content' => 'contenido',
'category_id' => 'categoría',
];
}
// Ahora los mensajes dirán:
// "El campo título es obligatorio."
// en lugar de "The title field is required."
Autorización con authorize()
El método authorize() determina si el usuario puede
realizar la acción. Si devuelve false, Laravel responde
con un error 403 (Forbidden):
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostRequest extends FormRequest
{
public function authorize(): bool
{
// Solo el autor puede editar su post
$post = $this->route('post');
return $this->user()->id === $post->user_id;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'content' => 'required|string',
];
}
}
Laravel genera authorize() retornando false
por seguridad. Si no necesitas autorización, cámbialo a
return true;.
Acceder a parámetros de la ruta
Dentro del Form Request puedes acceder a los parámetros de la ruta
con $this->route(). Esto es útil para validaciones
que dependen del contexto:
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
// Obtener el usuario de la ruta (route model binding)
$user = $this->route('user');
return [
'name' => 'required|string|max:255',
'email' => [
'required',
'email',
// Ignorar el email del usuario actual en la validación unique
Rule::unique('users', 'email')->ignore($user->id),
],
];
}
}
Preparar datos antes de validar
El método prepareForValidation() permite modificar los
datos antes de que se validen. Útil para normalizar o limpiar datos:
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
class StorePostRequest extends FormRequest
{
protected function prepareForValidation(): void
{
$this->merge([
// Generar slug a partir del título
'slug' => Str::slug($this->title),
// Normalizar el email a minúsculas
'email' => strtolower($this->email),
]);
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'slug' => 'required|string|unique:posts,slug',
'email' => 'required|email',
];
}
}
Ejemplo completo: CRUD de artículos
Veamos cómo usar Form Requests para crear y actualizar artículos:
<?php
// app/Http/Requests/StoreArticleRequest.php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreArticleRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'slug' => 'required|string|unique:articles,slug',
'excerpt' => 'nullable|string|max:500',
'content' => 'required|string',
'published_at' => 'nullable|date',
];
}
public function messages(): array
{
return [
'title.required' => 'El título es obligatorio.',
'slug.required' => 'El slug es obligatorio.',
'slug.unique' => 'Este slug ya está en uso.',
'content.required' => 'El contenido es obligatorio.',
];
}
}
<?php
// app/Http/Requests/UpdateArticleRequest.php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateArticleRequest extends FormRequest
{
public function authorize(): bool
{
// Solo el autor puede editar
$article = $this->route('article');
return $this->user()->id === $article->user_id;
}
public function rules(): array
{
$article = $this->route('article');
return [
'title' => 'required|string|max:255',
'slug' => [
'required',
'string',
Rule::unique('articles', 'slug')->ignore($article->id),
],
'excerpt' => 'nullable|string|max:500',
'content' => 'required|string',
'published_at' => 'nullable|date',
];
}
}
<?php
// app/Http/Controllers/ArticleController.php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\StoreArticleRequest;
use App\Http\Requests\UpdateArticleRequest;
use App\Models\Article;
use Illuminate\Http\RedirectResponse;
class ArticleController extends Controller
{
public function store(StoreArticleRequest $request): RedirectResponse
{
$article = $request->user()->articles()->create($request->validated());
return redirect()->route('articles.show', $article);
}
public function update(UpdateArticleRequest $request, Article $article): RedirectResponse
{
$article->update($request->validated());
return redirect()->route('articles.show', $article);
}
}
Resumen
php artisan make:request NombreRequestcrea un Form Requestrules()define las reglas de validaciónmessages()personaliza los mensajes de errorattributes()personaliza los nombres de los camposauthorize()controla quién puede realizar la acción$this->route('param')accede a parámetros de la rutaprepareForValidation()modifica datos antes de validar$request->validated()devuelve solo los datos validados
Ejercicios
Ejercicio 1: Form Request para productos
Crea un StoreProductRequest para validar productos con:
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 categories).
Incluye mensajes personalizados en español.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required|string|max:200',
'price' => 'required|numeric|min:0.01',
'stock' => 'required|integer|min:0',
'category_id' => 'required|exists:categories,id',
];
}
public function messages(): array
{
return [
'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.',
];
}
}
Ejercicio 2: Form Request con autorización
Crea un UpdateCommentRequest que solo permita al autor
del comentario editarlo. El comentario debe tener un campo
body (obligatorio, máximo 1000 caracteres).
Usa $this->route('comment') para obtener el comentario.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCommentRequest extends FormRequest
{
public function authorize(): bool
{
$comment = $this->route('comment');
return $this->user()->id === $comment->user_id;
}
public function rules(): array
{
return [
'body' => 'required|string|max:1000',
];
}
public function messages(): array
{
return [
'body.required' => 'El comentario no puede estar vacío.',
'body.max' => 'El comentario no puede superar los 1000 caracteres.',
];
}
}
Ejercicio 3: Form Request con prepareForValidation
Crea un StoreTagRequest para etiquetas que:
antes de validar, convierta el name a minúsculas
y genere un slug a partir del nombre.
Valida que name sea obligatorio (máximo 50) y
slug sea único en la tabla tags.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
class StoreTagRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$name = strtolower($this->name);
$this->merge([
'name' => $name,
'slug' => Str::slug($name),
]);
}
public function rules(): array
{
return [
'name' => 'required|string|max:50',
'slug' => 'required|string|unique:tags,slug',
];
}
public function messages(): array
{
return [
'name.required' => 'El nombre de la etiqueta es obligatorio.',
'name.max' => 'El nombre no puede superar los 50 caracteres.',
'slug.unique' => 'Ya existe una etiqueta con este nombre.',
];
}
}
¿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