yiisoft/middleware-dispatcher
A PSR-15 middleware dispatcher for Yii applications. Build and execute middleware pipelines with lazy resolution through the DI container, fallback request handlers, and support for individual middleware or grouped stacks. Integrates cleanly with Yii's request handling and the PSR-7/PSR-15 ecosystem...
composer require yiisoft/middleware-dispatcher
The package only gives you the dispatcher — you supply the ecosystem around it: a PSR-7/PSR-17 implementation, a PSR-11 container, and a terminal RequestHandlerInterface (your "kernel").
use Psr\Http\Server\RequestHandlerInterface;
use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher;
// $finalHandler runs when every middleware calls $handler->handle($request)
$dispatcher = new MiddlewareDispatcher($finalHandler, $container);
$dispatcher = $dispatcher->withMiddlewares([
ErrorHandlerMiddleware::class, // outermost — catches everything below
CorsMiddleware::class,
RouterMiddleware::class,
]);
$response = $dispatcher->handle($request);
Read in this order:
MiddlewareDispatcher::handle() — ~20 lines, it's the whole pipeline. Wraps definitions in reverse so the first entry is outermost.MiddlewareFactory::createMiddleware() — tells you exactly which definition shapes are legal (instance, class-string, callable/closure, [class, method]) and how the container is consulted.MiddlewareDefinition / the definitions collection — the escape hatch when you need parameters or lazy construction.InvalidMiddlewareDefinitionException — the failure mode you'll actually hit.First realistic use case: a BFF/API gateway or a Yii3 config/web/di.php-style bootstrap where you want a stack you can compose per route group and unit-test without a framework. Because it's plain PSR-15, the same stack runs under Yii3, Mezzio, Slim, or a Laravel Route:: shim.
Build once, dispatch many. The dispatcher is immutable and definitions are lazy, but the object graph still gets walked on each handle(). Register the assembled dispatcher as a container singleton; never call withMiddlewares() inside a controller.
Class-string definitions = constructor injection. This is the main reason to use the package over a hand-rolled foreach. Definitions are resolved through the PSR-11 container, so AuthMiddleware(LoggerInterface $log, AuthConfig $cfg) just works under yiisoft/di or Laravel's container. Closures and MiddlewareInterface instances skip the factory entirely.
Per-route / per-group pipelines. Keep a array<string, list<definition>> map in config and build a dispatcher per group at bootstrap. Cheap, and each group gets a different terminal handler:
$apiV1 = (new MiddlewareDispatcher($apiKernel, $container))
->withMiddlewares($config['pipelines']['api_v1']);
Nesting via a composite handler. To get "group middleware then route middleware", write a tiny RequestHandlerInterface whose handle() calls an inner dispatcher. That gives you true nested pipelines with distinct error boundaries — hard to do with a flat stack.
Conditional/lazy middleware. Because instantiation happens only when the pipeline reaches that point, put RoutingMiddleware near the top and expensive middleware (auth, DB session) behind it. Anything after a short-circuiting middleware never gets constructed.
Reuse foreign middleware. PSR-15 means laminas, Slim, and Mezzio middleware drop straight in. For Laravel middleware you'll need an adapter that shims $next($request) onto $handler->handle($request) — write it once.
Testing. No framework needed: pass a stub terminal handler that returns a canned response, record the order middleware were entered/left, assert on it. This is the fastest way to catch ordering mistakes.
withMiddlewares() returns a clone. $dispatcher->withMiddlewares([...]); is a silent no-op. Always reassign. This is the single most common bug with this package.handle() and any "return JSON on error" logic never fires.MiddlewareInterface nor callable throws InvalidMiddlewareDefinitionException on the first request. Add a smoke test that dispatches each configured pipeline once in CI.LoggerInterface::debug() call before and after $handler->handle() in a scratch middleware; the nesting in the log is the real execution order, and it's the fastest way to spot a misordered stack.add() / dispatch() era became withMiddlewares() / handle(). Check which major you're pinning before copying snippets from blog posts — most search results are for v1.MiddlewareDefinition (definition + parameters) rather than inventing wrapper classes.null returns deep in the stack.How can I help you explore Laravel packages today?