Middleware & Artisan

Laravel Fundamentals
Course 1 ยท Chapter 8 ยท Middleware & Artisan

๐Ÿงต Middleware & Artisan

VerifyCsrfToken from the last chapter is itself a piece of middleware โ€” this closing chapter looks at the mechanism directly: Laravel's middleware pipeline, how it differs from Django's in a genuinely meaningful way, and a full circle back to Artisan and .env from Chapter 1.

The Middleware Pipeline

Same "onion" model as Django's middleware chain โ€” a request passes through each layer before reaching the route, and the response passes back through in reverse. Laravel expresses each layer as a class with a handle() method, rather than Django's function-returning-a-function pattern:

php artisan make:middleware LogRequests
// app/Http/Middleware/LogRequests.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class LogRequests
{
    public function handle(Request $request, Closure $next)
    {
        $start = microtime(true);

        $response = $next($request);  // calls the next layer โ€” the route, or the next middleware

        $duration = (microtime(true) - $start) * 1000;
        logger()->info("{$request->path()} took {$duration}ms");

        return $response;
    }
}

Django's Closure vs Laravel's Middleware Class

Django
def request_timing_middleware(get_response):
    def middleware(request):
        # before
        response = get_response(request)
        # after
        return response
    return middleware
Laravel
class LogRequests
{
    public function handle($request, Closure $next)
    {
        // before
        $response = $next($request);
        // after
        return $response;
    }
}

๐Ÿ“Œ Registering Middleware: A Real Default Difference

Here's a genuine behavioral difference, not just syntax: Django's MIDDLEWARE list applies to every request by default. Laravel middleware is opt-in per route unless explicitly added to a shared group:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'log.requests' => \App\Http\Middleware\LogRequests::class,
    ]);
})
// routes/web.php โ€” applied to ONE route
Route::get('/books', [BookController::class, 'index'])
    ->middleware('log.requests');

// or applied to a whole group (Chapter 2's Route::prefix pattern)
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});

Middleware Groups (web / api)

Laravel does apply a group of middleware globally to all web or api routes (session handling, CSRF, etc.) โ€” but anything beyond that default group is opt-in per route or per group, not automatically project-wide.

Django's Model: Opt-Out, Not Opt-In

A new entry in Django's MIDDLEWARE list runs on every request immediately โ€” there's no per-view "don't apply this one" short of writing custom logic inside the middleware itself.

Revisiting Artisan

Chapter 1 introduced Artisan as Laravel's manage.py โ€” a few more commands worth knowing before wrapping up this course:

php artisan config:cache   # combine all config/*.php files into one cached file โ€” production speed boost
php artisan route:cache    # cache the compiled route table โ€” skips re-parsing routes/web.php on every request
php artisan make:middleware LogRequests
php artisan make:request StoreBookRequest

config:cache and route:cache are genuinely Laravel-specific โ€” Django has nothing quite equivalent for its urls.py/settings.py, since those are re-evaluated per-process-start rather than needing an explicit cache-compilation step. Course 2's Deployment chapter builds on this.

โš™๏ธ Tying It Together: .env and config/

Full circle back to Chapter 1 โ€” every setting referenced loosely across this course traces back to one of these two places:

How .env Feeds config/

// .env
APP_DEBUG=true
DB_CONNECTION=mysql
DB_DATABASE=example_app

// config/app.php โ€” reads .env via env()
return [
    'debug' => env('APP_DEBUG', false),
];

// config/database.php โ€” same pattern
return [
    'connections' => [
        'mysql' => [
            'database' => env('DB_DATABASE', 'laravel'),
        ],
    ],
];

// Application code never reads .env directly โ€” it reads config():
config('app.debug');        // true
config('database.connections.mysql.database');

This is Laravel's equivalent of Django's settings.py tour from Course 1's final chapter โ€” except split across many small config/*.php files instead of one monolithic file, each reading its real values from .env via the env() helper.

๐Ÿ’ป Coding Challenges

Challenge 1: Write and Apply Custom Middleware

Generate a middleware called EnsureBookstoreIsOpen that returns a 503 response outside of 9amโ€“5pm (simulate with a fixed hour check), register it as an alias, and apply it only to routes under an /orders prefix โ€” not globally.

Goal: Practice writing middleware and deliberately scoping it to specific routes rather than the whole app.

โ†’ Solution

Challenge 2: Compare Middleware Application Models

Explain, in your own words, what would happen differently if Laravel applied every registered middleware globally by default (the way Django does) โ€” specifically for a middleware that adds a 200ms artificial delay for logging purposes.

Goal: Practice reasoning about why Laravel's opt-in model is a deliberate design choice, not an oversight.

โ†’ Solution

Challenge 3: Fix Middleware That Never Calls $next()

Given a custom middleware whose handle() method does some logging but never calls or returns $next($request), explain what happens when a request passes through it, and fix the bug.

Goal: Practice recognizing and fixing the single most common custom-middleware mistake.

โ†’ Solution

โš ๏ธ Gotcha: Forgetting to Call $next($request)

A middleware's handle() method must call $next($request) (and return its result) to pass control forward โ€” skip it, and the request simply never reaches the route, the controller, or any middleware after it in the pipeline. There's no error message pointing at the cause; the symptom is just "this route hangs or returns nothing," which can be confusing to trace back to one missing line in a middleware class that otherwise looks correct. Equally worth remembering as this course closes: middleware applied globally (or to the broad web group) runs on every matching request โ€” an expensive middleware meant for one specific feature should be scoped to that feature's routes specifically, not left in the global stack "just in case."

๐ŸŽ“ Course Complete

That closes Laravel Fundamentals. Across eight chapters, Laravel occupied a similar "batteries-included" niche to Django โ€” an ORM, templating, routing, and CLI tooling all included from the first command โ€” while making its own distinct choices along the way: hand-written migrations instead of auto-diffed ones, relationships as explicit methods rather than declarative fields, route model binding eliminating manual lookups, and an opt-in middleware model rather than Django's global-by-default one. The mass-assignment defense ($fillable/validated()) that's now appeared in Rails, Django, DRF, and Laravel underscores how consistently the same real-world problems recur across frameworks, even when the syntax differs completely. Course 2 (Intermediate/Advanced) โ€” authentication, API Resources, testing with PHPUnit, events/listeners, queues, caching, authorization, and deployment โ€” is sketched and ready in the course bucket list whenever you want to continue.