Mastering Middleware in Laravel 12

Asfia AimanAsfia Aiman
6 min read

Laravel's middleware provides a convenient mechanism for filtering HTTP requests entering your application. From authentication to CSRF protection, middleware serves as the guardians of your application, ensuring requests meet specific criteria before reaching your application logic.

In Laravel 12, middleware execution order can be critical to your application functioning correctly. This article dives deep into managing middleware execution order, middleware parameters, terminable middleware, and manually managing Laravel's global middleware stack.

Understanding Middleware Execution Order

Middleware in Laravel executes in a specific sequence based on how they're registered. By default, middleware runs in the order they're added to a route:

Route::get('/profile', function () {
    // Your route logic
})->middleware(['auth', 'verified', 'log-activity']);

In this example, the auth middleware runs first, followed by verified, and finally log-activity. But what happens when you need a different order?

Sorting Middleware in Laravel 12

Sometimes, you need middleware to execute in a specific order regardless of how they're assigned to routes. Laravel 12 provides a powerful solution through the priority method in your application's bootstrap/app.php file:

->withMiddleware(function (Middleware $middleware) {
    $middleware->priority([
        \App\Http\Middleware\ImportantFirstMiddleware::class,
        \App\Http\Middleware\MustRunSecondMiddleware::class,
        \App\Http\Middleware\ThirdInLineMiddleware::class,
        // Other middleware in order of priority
    ]);
})

When to Use Middleware Priority?

You should use middleware priority when:

  1. Dependency Between Middleware: One middleware depends on another being executed first

  2. Global Order Requirements: Your application requires a consistent execution order across all routes

  3. Complex Middleware Chains: You have many middleware that need to interact in specific ways

Real-World Example

Let's say you're building a multi-tenant application with middleware that handles various aspects:

  • IdentifyTenant middleware identifies which tenant the request belongs to

  • SetupTenantDatabase middleware switches to the appropriate database connection

  • ApplyTenantTheme middleware adjusts UI elements based on tenant settings

In this case, IdentifyTenant must run before SetupTenantDatabase, which must run before ApplyTenantTheme. You can ensure this with:

->withMiddleware(function (Middleware $middleware) {
    $middleware->priority([
        \App\Http\Middleware\IdentifyTenant::class,
        \App\Http\Middleware\SetupTenantDatabase::class,
        \App\Http\Middleware\ApplyTenantTheme::class,
        // Other middleware
    ]);
})

What Happens Without Proper Sorting?

Without proper middleware sorting:

  1. Broken Functionality: Features dependent on middleware execution order may fail

  2. Data Inconsistencies: Data transformations might be applied incorrectly

  3. Security Vulnerabilities: Authentication or authorization checks might run after they're needed

  4. Performance Issues: Performance monitoring or caching might be ineffective

Middleware Parameters

Laravel 12 also allows middleware to receive additional parameters, making them more versatile.

How to Define Parametrized Middleware

Let's create a middleware that restricts access based on user subscription level:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class RequireSubscription
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next, string $level): Response
    {
        $user = $request->user();

        if (!$user || !$user->hasSubscription($level)) {
            return response()->json([
                'error' => 'This feature requires a ' . ucfirst($level) . ' subscription',
                'upgrade_url' => route('subscriptions.upgrade')
            ], 403);
        }

        return $next($request);
    }
}

How to Use Parametrized Middleware

You can pass parameters to middleware when defining routes:

Route::get('/premium-content', function () {
    // Premium content logic
})->middleware('subscription:premium');

Or using the class-based syntax:

use App\Http\Middleware\RequireSubscription;

Route::get('/analytics-dashboard', function () {
    // Analytics dashboard logic
})->middleware(RequireSubscription::class.':business');

For multiple parameters, separate them with commas:

Route::get('/developer-tools', function () {
    // Developer tools logic
})->middleware('subscription:premium,business');

Real-World Use Case for Middleware Parameters

Consider a content filtering middleware that adjusts what users can see based on age restrictions:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ContentFilter
{
    public function handle(Request $request, Closure $next, int $minimumAge = 13, bool $skipWarning = false): Response
    {
        $user = $request->user();
        $userAge = $user ? $user->age : null;

        // Guest users or underage users
        if (!$userAge || $userAge < $minimumAge) {
            if ($skipWarning) {
                return redirect()->route('content.restricted');
            }

            // Store original URL for after confirmation
            session(['intended_url' => $request->fullUrl()]);

            return redirect()->route('content.warning', [
                'minimum_age' => $minimumAge
            ]);
        }

        return $next($request);
    }
}

Usage:

// Default age restriction (13+)
Route::get('/forums', function () {
    // Forum content
})->middleware('content.filter');

// Mature content (18+) with direct block
Route::get('/mature-content', function () {
    // Mature content
})->middleware('content.filter:18,true');

// Teen content (16+) with warning
Route::get('/teen-content', function () {
    // Teen content 
})->middleware('content.filter:16,false');

Terminable Middleware

Sometimes, you need middleware to perform actions after the HTTP response has been sent to the browser. Laravel 12 supports this through terminable middleware.

How to Create Terminable Middleware

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;

class TrackPageViews
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): Response
    {
        // Pass through to controller action
        return $next($request);
    }

    /**
     * Handle tasks after the response has been sent to the browser.
     */
    public function terminate(Request $request, Response $response): void
    {
        // Skip AJAX requests and non-successful responses
        if ($request->ajax() || $response->getStatusCode() !== 200) {
            return;
        }

        // Only track GET requests to page views
        if ($request->method() !== 'GET') {
            return;
        }

        // Record the page view asynchronously after response is sent
        $path = $request->path();
        $userAgent = $request->userAgent();
        $userId = $request->user()?->id;

        DB::table('page_views')->insert([
            'path' => $path,
            'user_id' => $userId,
            'user_agent' => $userAgent,
            'ip_address' => $request->ip(),
            'viewed_at' => now(),
        ]);
    }
}

When to Use Terminable Middleware?

Terminable middleware is perfect for:

  1. Logging: Recording request/response data after the response is sent

  2. Queue Jobs: Dispatching background jobs based on request data

  3. Cleanup: Performing cleanup operations after request processing

  4. Performance Tracking: Measuring and recording performance metrics

Important Note on Terminable Middleware

By default, Laravel creates a fresh instance of middleware for the terminate method. If you want to use the same instance for both handle and terminate, register the middleware as a singleton:

// In AppServiceProvider.php
public function register(): void
{
    $this->app->singleton(\App\Http\Middleware\LogResponseTime::class);
}

Manually Managing Laravel's Default Global Middleware

Laravel 12 offers enhanced control over the global middleware stack through the bootstrap/app.php file.

Customizing the Global Middleware Stack

->withMiddleware(function (Middleware $middleware) {
    $middleware->use([
        \Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class,
        \Illuminate\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Http\Middleware\ValidatePostSize::class,
        \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        // Your custom global middleware here
    ]);
})

Replacing Default Middleware

You can replace Laravel's default middleware with your own custom implementations:

use App\Http\Middleware\CustomSessionHandler;
use Illuminate\Session\Middleware\StartSession;

$middleware->web(replace: [
    StartSession::class => CustomSessionHandler::class,
]);

Removing Default Middleware

If a default middleware doesn't suit your needs, you can remove it entirely:

$middleware->web(remove: [
    \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
]);

Best Practices for Laravel 12 Middleware Management

  1. Keep a Clear Order: Maintain a clear and documented middleware execution order

  2. Create Single-Purpose Middleware: Each middleware should have one clear responsibility

  3. Use Group-Specific Middleware: Use middleware groups for route-specific middleware

  4. Optimize Performance: Be mindful of middleware overhead, especially on high-traffic routes

  5. Handle Errors Appropriately: Middleware should handle exceptions gracefully

  6. Test Middleware Chains: Thoroughly test how your middleware interacts in chains

Conclusion

Understanding and controlling middleware execution order in Laravel 12 is crucial for building robust applications. Whether you're handling authentication, localization, or custom business logic, properly sorting middleware ensures your application behaves predictably and securely.

The ability to define execution priority, pass parameters, and execute code after sending responses makes Laravel middleware a powerful tool in your development arsenal. By mastering these concepts, you'll create more maintainable, secure, and efficient Laravel applications.

Remember that middleware is part of your application's foundation - investing time in properly organizing it pays dividends in the long run through reduced bugs, better security, and more maintainable code.

What's your experience with Laravel middleware? Have you encountered situations where middleware execution order was critical? Share your thoughts in the comments below!

0
Subscribe to my newsletter

Read articles from Asfia Aiman directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Asfia Aiman
Asfia Aiman

Hey Hashnode community! I'm Asfia Aiman, a seasoned web developer with three years of experience. My expertise includes HTML, CSS, JavaScript, jQuery, AJAX for front-end, PHP, Bootstrap, Laravel for back-end, and MySQL for databases. I prioritize client satisfaction, delivering tailor-made solutions with a focus on quality. Currently expanding my skills with Vue.js. Let's connect and explore how I can bring my passion and experience to your projects! Reach out to discuss collaborations or learn more about my skills. Excited to build something amazing together! If you like my blogs, buy me a coffee here https://www.buymeacoffee.com/asfiaaiman