Lección 25 de 45 12 min de lectura

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ón
  • rules(): 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:

bash
php artisan make:request StorePostRequest

Esto crea el archivo app/Http/Requests/StorePostRequest.php:

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',
        ];
    }
}
Convención de nombres

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
<?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
<?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
<?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
<?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',
        ];
    }
}
authorize() por defecto

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
<?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
<?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
<?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
<?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
<?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 NombreRequest crea un Form Request
  • rules() define las reglas de validación
  • messages() personaliza los mensajes de error
  • attributes() personaliza los nombres de los campos
  • authorize() controla quién puede realizar la acción
  • $this->route('param') accede a parámetros de la ruta
  • prepareForValidation() 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.',
        ];
    }
}

¿Te está gustando el curso?

Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.

Descubrir cursos premium