$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[October 01, 2025] Laravel

Middleware in Laravel: A Deep Dive

Middleware in Laravel: Tips & Tricks

────────────────────────────────────────────────────────

Middleware in Laravel: Tips & Tricks

Tip: Middleware Order Matters

Middleware runs in the order you register it. Put authentication before authorization:

$middleware->append([
    CheckAuth::class,      // runs first
    CheckPermission::class, // runs second
]);

Gotcha: Global Middleware Runs on EVERY Request

Even on static assets if they go through Laravel. Be careful with heavy middleware in the global stack.

Tip: Use Middleware Groups for API Versioning

$middleware->appendToGroup('api:v1', [
    ApiV1Compatibility::class,
]);

$middleware->appendToGroup('api:v2', [
    ApiV2Compatibility::class,
]);

Tip: Terminable Middleware for Logging

public function terminate(Request $request, Response $response): void
{
    Log::info('Request completed', [
        'method' => $request->method(),
        'status' => $response->getStatusCode(),
        'duration' => microtime(true) - LARAVEL_START,
    ]);
}

Gotcha: Middleware Singletons

If you want the same middleware instance for handle() and terminate(), register it as a singleton:

$this->app->singleton(TerminatingMiddleware::class);

Otherwise Laravel creates a fresh instance for terminate().

Tip: Skip Middleware for Specific Routes

Route::middleware([AuthMiddleware::class])->group(function () {
    Route::get('/dashboard', ...);
    Route::get('/public', ...)->withoutMiddleware([AuthMiddleware::class]);
});

Tip: Middleware Parameters for Role Checking

public function handle(Request $request, Closure $next, string ...$roles): Response
{
    if (! $request->user()->hasAnyRole($roles)) {
        abort(403);
    }
    return $next($request);
}

// Usage:
Route::get('/admin', ...)->middleware('role:admin,superadmin');

Tip: Use route:cache Carefully

php artisan route:cache is fast, but it doesn't work with closure-based routes. Every time you cache routes, Laravel serializes them. If you have Route::redirect() or closure callbacks, the cache breaks. Stick to controller-based routes in production.

Tip: Model APP_KEY Rotation

Rotating APP_KEY invalidates all encrypted data — cookies, encrypted DB columns, and password reset tokens. If you must rotate (e.g., after a leak), plan a migration that re-encrypts existing data with the new key.

Gotcha: Local Scope Leaks

Global scopes defined in booted() apply to ALL queries on that model — including relationships. An innocent User::all() in admin panel might exclude soft-deleted users if a global scope is active.

Senior Insight

Middleware ordering is consistently one of the top five sources of subtle bugs I've encountered in Laravel codebases. I've seen entire features break because rate-limiting ran after the controller logic, or because CORS headers were applied in the wrong position. My rule after years of debugging this: request-modifying middleware (CORS, session, TrimStrings) goes first, then authentication, then authorization, and finally rate-limiting. Draw a pipeline diagram before writing a single middleware class.

Source: Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/laravel), Spatie Blog (https://spatie.be/blog)

────────────────────────────────────────────────────────
<-- back to posts