Created
December 13, 2022 11:32
-
-
Save mindplay-dk/714f653c5f4f38002111877226e3adbd to your computer and use it in GitHub Desktop.
PSR Handlers as middleware
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use Psr\Http\Message\ResponseFactoryInterface; | |
use Psr\Http\Message\ServerRequestInterface; | |
use Psr\Http\Message\ResponseInterface; | |
use Psr\Http\Server\MiddlewareInterface; | |
use Psr\Http\Server\RequestHandlerInterface; | |
// There are essentially 3 handler/middlerware patterns: | |
// | |
// 1. Delegating to the next handler/middleware, then altering the response | |
class PoweredByHandler implements RequestHandlerInterface | |
{ | |
public function __construct( | |
private string $powered_by, | |
private RequestHandlerInterface $next | |
) { | |
} | |
public function handle(ServerRequestInterface $request): ResponseInterface | |
{ | |
return $this->next->handle($request) | |
->withAddedHeader("X-Powered-By", $this->powered_by); | |
} | |
} | |
class PoweredByMiddleware implements MiddlewareInterface | |
{ | |
public function __construct( | |
private string $powered_by | |
) { | |
} | |
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface | |
{ | |
return $next->handle($request) | |
->withAddedHeader("X-Powered-By", $this->powered_by); | |
} | |
} | |
// 2. Modifying the request, then delegating to the next handler/middleware | |
class Router | |
{ | |
public function resolve(ServerRequestInterface $request): string | |
{ | |
return "/lol"; | |
} | |
} | |
class RouterHandler implements RequestHandlerInterface | |
{ | |
public function __construct( | |
private Router $router, | |
private RequestHandlerInterface $next | |
) { | |
} | |
public function handle(ServerRequestInterface $request): ResponseInterface | |
{ | |
$route = $this->router->resolve($request); | |
return $this->next->handle($request->withAttribute("route", $route)); | |
} | |
} | |
class RouterMiddleware implements MiddlewareInterface | |
{ | |
public function __construct( | |
private Router $router | |
) { | |
} | |
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface | |
{ | |
$route = $this->router->resolve($request); | |
return $next->handle($request->withAttribute("route", $route)); | |
} | |
} | |
// 3. Handling the request without delegating to the next handler/middleware | |
class NotFoundHandler implements RequestHandlerInterface | |
{ | |
public function __construct( | |
private ResponseFactoryInterface $responseFactory | |
) { | |
} | |
public function handle(ServerRequestInterface $request): ResponseInterface | |
{ | |
return $this->responseFactory->createResponse(404); | |
} | |
} | |
class NotFoundMiddleware implements MiddlewareInterface | |
{ | |
public function __construct( | |
private ResponseFactoryInterface $responseFactory | |
) { | |
} | |
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface | |
{ | |
return $this->responseFactory->createResponse(404); | |
} | |
} | |
// The handler and middleware versions are mostly identical. | |
// | |
// One notable difference with examples (1) and (2) is that handlers receive | |
// all their dependencies in the constructor - whereas, with the middleware, | |
// the `$next` handler is instead provided when the middleware is invoked. | |
// | |
// With example (3) we see a problem with the middleware interface - this | |
// middleware never delegates, and therefore receives an unused `$next` | |
// argument. The constructor for the handler version has no dependency on | |
// any `$next` handler and better expresses it's real dependencies. | |
// | |
// It's interesting to note how middleware stacks get around this issue: | |
// what do you do when you reach the last middleware in the stack, and there | |
// is no `$next` handler? Generally, you insert a built-in dummy handler, | |
// which might either throw an exception or return a 500 response, etc. | |
// | |
// Handlers do not have any of these problems. | |
// | |
// It's difficult to look at these examples and argue that middleware is better. | |
// | |
// The only meaningful argument for middleware is cosmetics - the idea of | |
// middleware that composes easily and elegantly is appealing on the surface | |
// because of it's linear nature: | |
$handler = new Dispatcher([ | |
new RouterMiddleware($router), | |
new PoweredByMiddleware("PHP"), | |
new NotFoundMiddleware($responseFactory), | |
]); | |
// The composition of bare handlers is perceived as less elegant because | |
// of it's nested layout, which appears more cluttered on the surface: | |
$handler = new RouterHandler( | |
$router, | |
new PoweredByHandler( | |
"PHP", | |
new NotFoundHandler($responseFactory) | |
) | |
); | |
// If you have many middlewares, you will most likely need to lazy-load them. | |
// | |
// This can be achieved by wrapping each middleware in a constructor delegate: | |
class LazyMiddleware implements MiddlewareInterface | |
{ | |
private ?MiddlewareInterface $middleware; | |
/** | |
* @param callable():MiddlewareInterface $create | |
*/ | |
public function __construct( | |
private callable $create | |
) { | |
} | |
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface | |
{ | |
if (! $this->middleware) { | |
$this->middleware = ($this->create)(); | |
} | |
return $this->middleware->process($request, $next); | |
} | |
} | |
// Assuming this `LazyMiddleware` is built-into our `Dispatcher`, composing the same | |
// middleware stack using constructor delegates looks like this: | |
$handler = new Dispatcher([ | |
fn () => new RouterMiddleware($router), | |
fn () => new PoweredByMiddleware("PHP"), | |
fn () => new NotFoundMiddleware($responseFactory), | |
]); | |
// We can apply the same pattern to handlers, by introducing a similar amount | |
// of framework: a `compose` function that accepts constructor delegates, and a | |
// `LazyHandler` to ensure we don't invoke these constructor delegates until needed: | |
/** | |
* @param array<callable(RequestHandlerInterface $next): RequestHandlerInterface> $handlers | |
* @param callable():RequestHandlerInterface $final | |
*/ | |
function compose(array $handlers, callable $final): RequestHandlerInterface | |
{ | |
$handler = $final(); | |
for ($i = count($handlers) - 1; $i >= 0; $i -= 1) { | |
$handler = new LazyHandler($handlers[$i], $handler); | |
} | |
return $handler; | |
} | |
class LazyHandler implements RequestHandlerInterface | |
{ | |
private ?RequestHandlerInterface $handler; | |
/** | |
* @param callable(RequestHandlerInterface $next): RequestHandlerInterface $create | |
* @param RequestHandlerInterface $next | |
*/ | |
public function __construct( | |
private callable $create, | |
private RequestHandlerInterface $next | |
) { | |
} | |
public function handle(ServerRequestInterface $request): ResponseInterface | |
{ | |
if (!$this->handler) { | |
$this->handler = ($this->create)($this->next); | |
} | |
return $this->handler->handle($request); | |
} | |
} | |
// This gives the appearance of a more linear "middleware stack": | |
$handler = compose( | |
[ | |
fn (RequestHandlerInterface $next) => new RouterHandler($router, $next), | |
fn (RequestHandlerInterface $next) => new PoweredByHandler("PHP", $next), | |
], | |
fn () => new NotFoundHandler($responseFactory) | |
); | |
// Note the absence of `$next` for the `$final` handler: this correctly expresses the | |
// fact that the handler at the bottom needs to be able to respond without having | |
// any `$next` handler to delegate to. No unused `$next` here. | |
// | |
// The `LazyHandler` ensures we don't run any of the constructor delegates until needed. | |
// It's an internal implementation detail of `compose`. (It doesn't leak.) | |
// | |
// Overall, I find this to be simpler and more elegant than introducing an unnecessary | |
// middleware interface - the extra degree of abstraction doesn't buy you anything, | |
// it clutters you dependencies, and it isn't correct for e.g. `NotFoundMiddleware`. | |
// | |
// I like simple things. | |
// | |
// I will continue to use the PSR middleware standard because the community does, but I | |
// wish we hadn't invented this extra layer of complexity to begin with. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment