Form Requests & Advanced Validation

Move validation out of controllers into dedicated, reusable, authorizable request classes.

Inline $request->validate([...]) is fine for a toy, but real endpoints accumulate rules, authorization, and data massaging. A Form Request is a dedicated class that owns all of it, leaving your controller method a clean two-liner.

Three methods do the work

  • authorize() — return true/false (or a Gate check). If it fails, a 403 before rules even run.
  • rules() — the validation rules, where you can use array/object rule builders.
  • messages() / attributes() — humane error text.
class StorePostRequest extends FormRequest
{
    public function authorize(): bool { return $this->user()->can('create', Post::class); }
    public function rules(): array {
        return ['title' => ['required', 'string', 'max:255']];
    }
}

// Controller: public function store(StorePostRequest $r) { Post::create($r->validated()); }

Beyond simple rules

Use prepareForValidation() to normalise input before rules run, withValidator() for cross-field checks with after(), and rule objects like Rule::unique(...)->ignore($id) for context-aware constraints.

Example

Example · php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;

class UpdateProjectRequest extends FormRequest
{
    public function authorize(): bool
    {
        // Route-model-bound {project}; policy decides.
        return $this->user()->can('update', $this->route('project'));
    }

    protected function prepareForValidation(): void
    {
        // Normalise BEFORE rules run.
        $this->merge(['slug' => \Illuminate\Support\Str::slug((string) $this->name)]);
    }

    public function rules(): array
    {
        return [
            'name'  => ['required', 'string', 'max:120'],
            'slug'  => ['required', Rule::unique('projects')->ignore($this->route('project'))],
            'budget'=> ['nullable', 'integer', 'min:0'],
            'tags'  => ['array', 'max:10'],
            'tags.*'=> ['string', 'distinct'],
        ];
    }

    public function withValidator(Validator $validator): void
    {
        $validator->after(function (Validator $v) {
            if ($this->budget && $this->budget > $this->user()->budget_cap) {
                $v->errors()->add('budget', 'Exceeds your approval limit.');
            }
        });
    }
}

// Controller stays tiny:
// public function update(UpdateProjectRequest $r, Project $project) {
//     $project->update($r->validated());
// }

When to use it

  • A developer moves a 20-line validate() block out of a controller into a StoreProductRequest class so the controller method is reduced to a single validated line.
  • An authorization check in authorize() rejects the request with a 403 if the authenticated user does not own the resource being updated, before rules() even runs.
  • A form request's prepareForValidation() hook normalises the 'price' field from a comma-formatted string to a float before the numeric rule validates it.

More examples

Complete Form Request class

authorize() gates the request by policy; rules() declares typed validation constraints — all before the controller method runs.

Example · php
class UpdateProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('update', $this->route('product'));
    }

    public function rules(): array
    {
        return [
            'name'  => ['required', 'string', 'max:200'],
            'price' => ['required', 'numeric', 'min:0'],
            'stock' => ['required', 'integer', 'min:0'],
            'tags'  => ['array'],
            'tags.*'=> ['string', 'exists:tags,slug'],
        ];
    }
}

Use the Form Request in a controller

Type-hinting the Form Request triggers automatic validation and authorization; validated() returns only the safe, declared fields.

Example · php
public function update(
    UpdateProductRequest $request,
    Product $product
): RedirectResponse {
    // $request->validated() contains only the declared rule keys
    $product->update($request->validated());

    return redirect()->route('products.show', $product)
        ->with('success', 'Product updated.');
}

Normalise input with prepareForValidation()

prepareForValidation() fires before rules() and lets you sanitise or transform input data so the validation rules receive clean values.

Example · php
class StoreOrderRequest extends FormRequest
{
    protected function prepareForValidation(): void
    {
        // Convert '1,299.99' -> 1299.99 before validation
        $this->merge([
            'total' => (float) str_replace(',', '', $this->total),
            'email' => strtolower(trim($this->email)),
        ]);
    }

    public function rules(): array
    {
        return [
            'total' => ['required', 'numeric', 'min:0.01'],
            'email' => ['required', 'email'],
        ];
    }
}

Discussion

  • Be the first to comment on this lesson.