Week 10: Controllers, Requests & Validation

Closures in routes/web.php work fine for a demo, but every real route needs a proper home for its logic. This week moves handlers into controllers following Laravel's RESTful conventions, and replaces the manual validation from Week 2 with Laravel's validation system.

Module 10 of 28 Week 10 of 32 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Build resource controllers following RESTful naming conventions
  • Read input & handle file uploads through the Request object
  • Validate input with rules & Form Request classes

1. Resource Controllers

A resource controller groups the seven conventional actions for managing one kind of resource — list, show a form to create, store, show one, show a form to edit, update, delete — under standardized method names:

terminal
php artisan make:controller TaskController --resource
app/Http/Controllers/TaskController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function index()          { /* GET /tasks -- list */ }
    public function create()         { /* GET /tasks/create -- new-item form */ }
    public function store(Request $request) { /* POST /tasks -- save new item */ }
    public function show(int $id)    { /* GET /tasks/{id} -- one item */ }
    public function edit(int $id)    { /* GET /tasks/{id}/edit -- edit form */ }
    public function update(Request $request, int $id) { /* PUT/PATCH /tasks/{id} */ }
    public function destroy(int $id) { /* DELETE /tasks/{id} */ }
}

One route registers all seven, wired to their conventional HTTP verb + URI:

routes/web.php
<?php

use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;

Route::resource('tasks', TaskController::class);
// Registers all 7 routes at once, all correctly named (tasks.index, tasks.store, ...)

Run php artisan route:list after adding this and you'll see exactly which URI, verb and name Laravel assigned to each of the seven methods — worth doing once so the convention isn't a black box.

2. The Request Object

Instead of reading $_POST/$_GET directly, Laravel injects a Request object into your controller method — type-hint it and Laravel supplies the current request automatically:

TaskController.php — store()
<?php

public function store(Request $request)
{
    $title = $request->input('title');        // like $_POST['title'] ?? null
    $title = $request->input('title', '');     // with a default
    $isUrgent = $request->boolean('urgent');   // "1"/"true"/"on" -> real bool
    $all = $request->only(['title', 'notes']); // just these keys, as an array

    if ($request->hasFile('attachment')) {
        $path = $request->file('attachment')->store('attachments'); // handles the move for you
    }

    // ...
}

$request->file('attachment')->store('attachments') replaces the manual move_uploaded_file() dance from Week 5 — Laravel generates a safe random filename and handles the move, storing relative to storage/app/ by default.

3. Validation Rules

$request->validate() replaces the manual if-checks from Week 2's contact form with declarative rules — invalid input automatically redirects back with error messages, with zero manual branching:

TaskController.php — store(), validated
<?php

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:200',
        'due_date' => 'nullable|date|after:today',
        'priority' => 'required|in:low,medium,high',
    ]);

    Task::create($validated); // Week 11 covers Eloquent's create() properly

    return redirect()->route('tasks.index')->with('success', 'Task created!');
}

If validation fails, validate() throws internally, and Laravel's exception handling catches it, redirects back to the previous page, and flashes both the errors and the old input into the session automatically — a Blade view reads them with @error('title') and old('title'):

resources/views/tasks/create.blade.php — excerpt
<input name="title" value="{{ old('title') }}">
@error('title')
  <p class="error">{{ $message }}</p>
@enderror

This is exactly the "re-fill the form, show the error" pattern built by hand in Week 2 — Laravel automates the plumbing, but the underlying idea (and the reason it exists) is unchanged.

4. Form Requests

Once a controller has several validated actions, inline rule arrays get repetitive. A Form Request extracts validation (and authorization) into its own class:

terminal
php artisan make:request StoreTaskRequest
app/Http/Requests/StoreTaskRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreTaskRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // Week 13 replaces this with a real authorization check
    }

    public function rules(): array
    {
        return [
            'title' => 'required|string|max:200',
            'due_date' => 'nullable|date|after:today',
            'priority' => 'required|in:low,medium,high',
        ];
    }
}
TaskController.php — using it
<?php

use App\Http\Requests\StoreTaskRequest;

public function store(StoreTaskRequest $request)
{
    // Validation already ran before this line executes -- $request->validated()
    // returns only the fields that passed the rules() array above.
    Task::create($request->validated());

    return redirect()->route('tasks.index')->with('success', 'Task created!');
}

Type-hinting StoreTaskRequest instead of the base Request is enough — Laravel runs its rules() (and, once authorization logic exists, its authorize()) before the method body executes at all. The controller method never even runs if validation fails.

5. Redirects & Flash Data

->with('success', '...') in the examples above flashes a one-time session value — the same concept built by hand in Week 5, now a built-in helper:

resources/views/layouts/app.blade.php — excerpt
@if (session('success'))
  <div class="alert">{{ session('success') }}</div>
@endif

Reading session('success') once and letting it naturally clear on the next request is exactly the manual flash() helper from Week 5, minus the code you'd otherwise have to write and maintain yourself.

6. Hands-on Exercise

Hands-on

Convert Week 9's routes into a resource controller

Move last week's closures into a proper controller with real validation, still on hardcoded in-memory data (Eloquent arrives next week).

Requirements:

  1. Generate TaskController with --resource, and register it with Route::resource('tasks', TaskController::class).
  2. Move last week's hardcoded task array into a static property on the controller (temporary — Week 11 replaces this with a real database).
  3. Build a StoreTaskRequest Form Request with rules for title (required, max 200 chars) and priority (required, one of low/medium/high).
  4. Wire create.blade.php and store() together so submitting an invalid form redirects back showing errors via @error and re-fills fields via old().
  5. On successful creation, redirect to tasks.index with a flashed success message, displayed once at the top of the layout.
Hint

A static property is a deliberate, temporary stand-in for a real data store — it will not persist correctly across separate PHP-FPM/CLI-server processes in production. Treat it as a placeholder that Week 11's Eloquent models properly replace, not a pattern to keep.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What does Route::resource('tasks', TaskController::class) register in one line?

All seven conventional RESTful routes — index, create, store, show, edit, update and destroy — each mapped to the correct HTTP verb, URI pattern and controller method, and each pre-named following the tasks.* convention (tasks.index, tasks.store, and so on).

Q2

What happens if $request->validate([...]) fails inside a controller method?

It throws a ValidationException internally, which Laravel's exception handler catches automatically — the rest of the method never runs. For a normal web request, this redirects the user back to the previous page with the validation errors and their submitted input both flashed into the session, ready to be read with @error and old().

Q3

Why move validation rules into a Form Request class instead of leaving them inline in store()?

It separates validation (and eventually authorization) concerns from the controller's actual business logic, and the same rule set can be reused if another action needs identical validation. Just as importantly, it runs validation before the controller method body even executes, guaranteeing the method only ever runs with already-valid data — there's no way to accidentally skip the check.

Q4

Is ->with('success', 'Task created!') a new concept, or a Laravel-provided version of something built by hand earlier in this course?

It's the same flash-message concept implemented by hand with $_SESSION in Week 5 — a value stored in the session that's meant to be read exactly once, typically to show a confirmation after a redirect. Laravel's session('success') read helper handles the "read once, then clear" mechanics automatically, the same job the hand-written flash() function did back then.