Form Requests & Validation

Laravel Fundamentals
Course 1 ยท Chapter 7 ยท Form Requests & Validation

๐Ÿ“ Form Requests & Validation

This chapter mirrors Django's Forms & Validation chapter closely โ€” validating submitted data and protecting every form against CSRF are both handled here without a separate package, the same "included from day one" story. The shape differs: Laravel's validation runs through a dedicated FormRequest class injected by type-hint, running before the controller method body even executes.

Inline Validation

For a simple case, $request->validate() runs right inside a controller method โ€” rules expressed as pipe-separated strings:

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'price' => 'required|numeric|min:0',
        'author_id' => 'required|exists:authors,id',
    ]);

    Book::create($validated);
    return redirect()->route('books.index');
}

A failed validation automatically redirects back to the previous page with the errors and old input flashed to the session โ€” no manual if/else branch needed the way Django's is_valid() requires.

๐Ÿ“‹ FormRequest Classes

For anything beyond the simplest case, a dedicated request class keeps validation rules out of the controller entirely:

php artisan make:request StoreBookRequest
// app/Http/Requests/StoreBookRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreBookRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;  // or a real permission check
    }

    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'price' => 'required|numeric|min:0',
            'author_id' => 'required|exists:authors,id',
        ];
    }
}
// app/Http/Controllers/BookController.php
use App\Http\Requests\StoreBookRequest;

public function store(StoreBookRequest $request)
{
    // By the time this method body runs, validation has ALREADY passed โ€”
    // an invalid request never reaches this line at all.
    Book::create($request->validated());
    return redirect()->route('books.index');
}

Django's is_valid() vs Laravel's Type-Hinted FormRequest

Django
form = BookForm(request.POST)
if form.is_valid():
    form.save()
    return redirect("book-list")
# explicit check โ€” invalid path handled by falling through
Laravel
public function store(StoreBookRequest $request)
{
    Book::create($request->validated());
    return redirect()->route('books.index');
}
// no explicit check โ€” invalid requests never reach the method body

๐Ÿ›ก๏ธ Built-in CSRF Protection

The exact same "already on by default" story as Django's CSRF chapter โ€” VerifyCsrfToken middleware is already active, and Blade's @csrf directive is the one-line template requirement:

<form method="POST" action="{{ route('books.store') }}">
  @csrf
  <input type="text" name="title">
  <button type="submit">Save</button>
</form>

@csrf compiles to a hidden _token input field, matched against a token stored in the session โ€” the same synchronizer token pattern from the dedicated CSRF course, just expressed as one Blade directive instead of Django's {% csrf_token %}.

Displaying Validation Errors

An $errors variable is automatically shared with every view after a failed validation redirect โ€” no controller code needed to pass it in:

<form method="POST" action="{{ route('books.store') }}">
  @csrf
  <input type="text" name="title" value="{{ old('title') }}">
  @error('title')
    <p class="error">{{ $message }}</p>
  @enderror
  <button type="submit">Save</button>
</form>

old('title')

Refills a field with the previously submitted value after a failed validation โ€” so the user doesn't have to retype everything just because one field was wrong.

@error / @enderror

Renders its content only if that specific field failed validation, with $message holding the actual error text โ€” no manual if 'title' in errors check needed.

Two Layers of the Same Defense: validated() + $fillable

$request->validated() returns only the fields that were actually declared in rules() โ€” paired with Chapter 5's $fillable, this is the same two-layer mass-assignment defense Django's Forms chapter built with ModelForm.Meta.fields:

// Even if an attacker adds an extra "is_admin" field to the request body:
Book::create($request->validated());
// validated() only returns keys declared in rules() โ€” "is_admin" was never
// declared there, so it's dropped before it ever reaches create(). And even
// if it somehow got through, Book's own $fillable would block it too.

๐Ÿ’ป Coding Challenges

Challenge 1: Write Inline Validation

Write a controller method that validates a submitted name (required, string, max 100 chars) and bio (optional string) using $request->validate(), then creates an Author from the validated data.

Goal: Practice the simplest validation form before introducing a dedicated request class.

โ†’ Solution

Challenge 2: Create a FormRequest

Generate an UpdateBookRequest with php artisan make:request, define rules() for title/price, and update a controller's update() method to use it via type-hint injection instead of inline validation.

Goal: Practice the full generate-rules-inject workflow for a dedicated request class.

โ†’ Solution

Challenge 3: Fix a Form Missing @csrf

Given a Blade <form method="POST"> that's missing @csrf and failing on every submission, fix it, and explain the exact HTTP status code Laravel returns for a CSRF failure (and how it differs from Django's).

Goal: Practice recognizing this chapter's most common first-timer mistake by its actual symptom.

โ†’ Solution

โš ๏ธ Gotcha: A Missing @csrf Gives a 419, Not a 403

Omit @csrf from a POST form and every submission fails โ€” but Laravel's specific error is 419 Page Expired, not the 403 Django returns for the identical mistake. Same root cause (a missing/invalid CSRF token), different framework, different status code โ€” worth knowing the number specifically so a 419 in the browser's network tab immediately points at this, not a generic auth or permission problem. Separately: inside a FormRequest-validated controller method, always call $request->validated(), never $request->all() โ€” validation still ran either way, but all() throws away the allowlist benefit entirely, passing through any extra field an attacker included regardless of whether it was declared in rules().

๐ŸŽฏ What's Next

The final chapter of this course covers the two pieces holding everything together behind the scenes: Middleware & Artisan โ€” Laravel's middleware pipeline, more Artisan commands, and how .env configuration ties back to everything covered so far.