eDetector`.
- Consumers:
AuditTrail, PermissionChecker, DatabaseRouter.
Rule: A consumer must never execute before its provider.
Step 2: Define Pipeline Layers
Laravel's request lifecycle naturally supports layered execution. Structure your middleware into logical layers:
- Infrastructure Layer: CORS, TrustProxies, MaintenanceMode.
- Context Layer: WorkspaceContext, LocaleDetector.
- Security Layer: IdentityResolver, RateLimiter, PermissionChecker.
- Observability Layer: AuditTrail, RequestLogger.
Step 3: Implementation with Explicit Configuration
Modern Laravel applications (v11+) centralize middleware configuration in bootstrap/app.php. This allows for global enforcement of sequence rules using aliases and group manipulation.
New Code Example: Dependency-Aware Middleware
Instead of generic names, use intent-revealing interfaces. This clarifies the contract between middleware.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Context Provider: Injects workspace context into the request.
* Must execute before any middleware that queries tenant data.
*/
class WorkspaceContext
{
public function handle(Request $request, Closure $next): Response
{
$workspaceId = $this->resolveWorkspaceId($request);
// Inject context for downstream consumers
$request->attributes->set('workspace_context', [
'id' => $workspaceId,
'database' => $this->getDatabaseConnection($workspaceId),
]);
return $next($request);
}
private function resolveWorkspaceId(Request $request): string
{
// Logic to extract workspace ID from subdomain/header
return $request->header('X-Workspace-ID') ?? 'default';
}
private function getDatabaseConnection(string $workspaceId): string
{
return match($workspaceId) {
'default' => 'mysql',
default => "tenant_{$workspaceId}",
};
}
}
/**
* Context Consumer: Requires workspace context to route queries.
* Will fail or misroute if WorkspaceContext has not run.
*/
class DatabaseRouter
{
public function handle(Request $request, Closure $next): Response
{
$context = $request->attributes->get('workspace_context');
if (is_null($context)) {
throw new \RuntimeException('Workspace context is missing. Ensure WorkspaceContext middleware runs first.');
}
config(['database.default' => $context['database']]);
return $next($request);
}
}
Configuration Architecture:
Use bootstrap/app.php to enforce the sequence globally. This prevents route-level drift.
use App\Http\Middleware\WorkspaceContext;
use App\Http\Middleware\DatabaseRouter;
use App\Http\Middleware\IdentityResolver;
use App\Http\Middleware\AuditTrail;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
// Define aliases for clarity
$middleware->alias([
'workspace' => WorkspaceContext::class,
'db.router' => DatabaseRouter::class,
'auth.identity' => IdentityResolver::class,
'audit' => AuditTrail::class,
]);
// Enforce sequence in the 'api' group
// Order: Workspace -> DB Router -> Auth -> Audit
$middleware->appendToGroup('api', [
'workspace',
'db.router',
'auth.identity',
'audit',
]);
// Prepend infrastructure middleware to ensure it runs first
$middleware->prepend([
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\HandleCors::class,
]);
})
// ...
Rationale:
- Aliases: Decouple configuration from class names, allowing easier refactoring.
appendToGroup: Explicitly defines the sequence. Laravel executes middleware in the order they appear in the array.
prepend: Guarantees infrastructure middleware runs before application logic, regardless of group configuration.
- Context Injection: Using
$request->attributes is safer than $request->merge for internal context, as it avoids polluting the input data.
Pitfall Guide
Even with a structured approach, specific pitfalls can undermine the pipeline. Below are common mistakes derived from production experience, along with remediation strategies.
| Pitfall Name | Explanation | Fix |
|---|
| Authorization-Authentication Inversion | Permission checks (can, auth) run before the user identity is resolved. This results in 403 errors for valid users or null reference exceptions. | Ensure IdentityResolver or auth middleware is always placed before permission middleware in the sequence. |
| The Silent Context Void | A consumer middleware expects data (e.g., tenant ID) that hasn't been injected. The code may fallback to a default, causing cross-tenant data exposure without errors. | Implement strict validation in consumers. Throw an exception if required context is missing, rather than failing silently. |
| The Cache Mirage | Middleware configuration is updated, but Laravel's route or config cache serves the old sequence. Developers believe changes have no effect. | Always run php artisan optimize:clear after middleware changes. Integrate cache clearing into CI/CD pipelines. |
| Global Middleware Bloat | Heavy logic (e.g., database queries, complex calculations) is placed in global middleware, impacting every request including static assets and health checks. | Move business-specific logic to route groups. Keep global middleware limited to infrastructure concerns like CORS and trust proxies. |
| API/Session Bleed | Session-dependent middleware is applied to API routes, causing authentication failures or unnecessary session file creation. | Maintain strict separation between web and api groups. Never include session middleware in the api group. |
| Rate-Limit Race Condition | Rate limiting runs before authentication, causing anonymous abuse to consume limits or blocking authenticated users based on IP rather than user ID. | For user-based rate limiting, place auth middleware before the rate limiter. For IP-based limiting, rate limiting can run earlier. |
| Pipeline Tracing Blindness | When debugging, developers lack visibility into the actual execution order, leading to guesswork. | Implement pipeline tracing by logging middleware entry/exit points during development. |
Production Bundle
This section provides actionable artifacts to implement and maintain a robust middleware pipeline in production environments.
Action Checklist
Decision Matrix
Use this matrix to determine the optimal middleware strategy based on application requirements.
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Multi-Tenant SaaS | WorkspaceContext -> DatabaseRouter -> Auth -> Business | Ensures database connection is set before any query or auth check occurs. | Low (Configuration only). |
| Public API | TrustProxies -> RateLimit -> Auth -> Controller | Protects infrastructure from abuse before authentication overhead. | Low. |
| Admin Panel | Auth -> Permission -> Audit | Prioritizes security and auditability over performance. | Low. |
| Legacy Migration | Route::group with explicit middleware array | Allows gradual migration without refactoring global configuration. | Medium (Technical debt). |
Configuration Template
Copy this template into bootstrap/app.php for a Laravel 11+ application with a structured pipeline.
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
// 1. Define Aliases
$middleware->alias([
'workspace' => \App\Http\Middleware\WorkspaceContext::class,
'db.router' => \App\Http\Middleware\DatabaseRouter::class,
'auth' => \App\Http\Middleware\IdentityResolver::class,
'verified' => \App\Http\Middleware\EnsureEmailIsVerified::class,
'permission' => \App\Http\Middleware\PermissionChecker::class,
'audit' => \App\Http\Middleware\AuditTrail::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
]);
// 2. Infrastructure Layer (Prepended)
$middleware->prepend([
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\HandleCors::class,
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
]);
// 3. Web Group Configuration
$middleware->group('web', [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
// 4. API Group Configuration (Dependency-Aware Sequence)
$middleware->group('api', [
'workspace', // Provider: Sets tenant context
'db.router', // Consumer: Routes DB based on tenant
'throttle:api', // Rate limiting
'auth', // Provider: Resolves user identity
'verified', // Consumer: Checks email verification
'permission', // Consumer: Checks permissions (requires auth)
'audit', // Consumer: Logs activity (requires auth)
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
})
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->create();
Quick Start Guide
Get your middleware pipeline audited and running in under five minutes.
- Inspect Current Order: Run
php artisan route:list --path=api to view the middleware stack for your routes. Note any sequences that look incorrect.
- Add Tracing: Temporarily add
Log::debug('Executing: ' . get_class($this)); to the top of your middleware handle methods.
- Execute Test Request: Send a request to a protected route. Check the logs to verify the execution order matches your dependency map.
- Adjust Configuration: Update
bootstrap/app.php to correct any sequence violations. Use appendToGroup or prependToGroup to enforce order.
- Clear Caches: Run
php artisan optimize:clear to ensure the new configuration is active. Remove tracing logs once verified.