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.
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.
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.
$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.