0

Middleware resolution from the container

Advanced5 min read·fw-05-004

Concept

Middleware resolution from the container means middleware classes are registered as bindings in the IoC container and resolved (with their dependencies injected) when the pipeline processes a request. This enables middleware to have constructor dependencies.

Why container resolution matters: A CorsMiddleware might need a ConfigRepository. A ThrottleMiddleware needs a CacheInterface. Without container resolution, you'd have to instantiate all middleware manually with their dependencies.

Resolution approaches:

  1. String class names: Store middleware as 'App\Http\Middleware\AuthMiddleware'. Resolve via $container->make($className) at pipeline run time.
  2. Closures: Store as fn() => new AuthMiddleware($dependency). Call the closure to get the instance.
  3. Pre-instantiated objects: Already resolved objects. No container needed.

Lazy resolution: Only resolve middleware when the pipeline actually runs, not at registration time. This avoids initializing middleware that won't be needed.

Route-level vs global middleware: Global middleware runs on every request. Route-level middleware runs only on matched routes. The pipeline is built differently for each. Global middleware is added by the framework. Route middleware is registered by name (e.g., 'auth'AuthMiddleware::class) and resolved per-route.

Middleware groups: Named collections of middleware (e.g., 'web' includes session, CSRF; 'api' includes throttle). Groups are expanded when building the pipeline.

Code Example

php
<?php
namespace Framework\Http;

use Framework\Container\Container;
use Psr\Http\Server\MiddlewareInterface;

class MiddlewareRegistry
{
    private array $global      = [];  // always run — class names
    private array $named       = [];  // 'auth' => AuthMiddleware::class
    private array $groups      = [];  // 'web' => ['session', 'csrf', 'auth']

    public function __construct(private readonly Container $container) {}

    public function add(string $className): void
    {
        $this->global[] = $className;
    }

    public function alias(string $alias, string $className): void
    {
        $this->named[$alias] = $className;
    }

    public function group(string $name, array $middleware): void
    {
        $this->groups[$name] = $middleware;
    }

    public function resolve(string|array $middleware): array
    {
        $names = (array) $middleware;
        $resolved = [];

        foreach ($names as $name) {
            if (isset($this->groups[$name])) {
                // Expand group recursively
                $resolved = array_merge($resolved, $this->resolve($this->groups[$name]));
            } elseif (isset($this->named[$name])) {
                $resolved[] = $this->container->make($this->named[$name]);
            } elseif (class_exists($name)) {
                $resolved[] = $this->container->make($name);
            }
        }

        return $resolved;
    }

    public function buildGlobal(): array
    {
        return array_map(fn($class) => $this->container->make($class), $this->global);
    }
}

// Registration in bootstrap/app.php
$registry = new MiddlewareRegistry($container);

// Global middleware (every request)
$registry->add(\Framework\Http\Middleware\LoggingMiddleware::class);
$registry->add(\Framework\Http\Middleware\HandleCors::class);

// Named aliases (for route middleware)
$registry->alias('auth',      \App\Http\Middleware\Authenticate::class);
$registry->alias('throttle',  \App\Http\Middleware\RateLimiter::class);
$registry->alias('verified',  \App\Http\Middleware\EnsureEmailVerified::class);

// Groups
$registry->group('api', ['throttle', 'auth']);
$registry->group('web', ['session', 'csrf', 'auth']);

// Building pipeline for a request
$routeMiddleware = $registry->resolve($matchedRoute->middleware);
$globalMiddleware = $registry->buildGlobal();
$allMiddleware = array_merge($globalMiddleware, $routeMiddleware);