Form Requests & Validation
๐ Form Requests & Validation
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.
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.
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.
@csrf Gives a 419, Not a 403Omit @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.