Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk
Created December 13, 2022 11:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mindplay-dk/714f653c5f4f38002111877226e3adbd to your computer and use it in GitHub Desktop.
Save mindplay-dk/714f653c5f4f38002111877226e3adbd to your computer and use it in GitHub Desktop.
PSR Handlers as middleware
<?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