I hate to do this at a time when the middleware PSR is probably close to 
finished, but since this occurred to me, I can't shake the thought, and so 
I have to bring this up, so that at least others are aware and informed 
about this option with regards to middleware.

I think I saw some framework in Dart doing this a couple of months ago, and 
I instinctually rejected the idea, because (A) I have a substantial time 
investment in PSR-15, and (B) it's easy to dismiss things that appear to be 
too simple - but I would feel remiss if I didn't at least ponder it, so I 
have been, and I find (reluctantly) that it makes an awful lot of sense.

Consider the most generic interface possible for a contract like "takes a 
request, returns a response" - this:

interface Middleware {
    public function process(RequestInterface $request): ResponseInterface;
}

Okay, so that looks more like a front-controller or something - that can't 
be "middleware", because there's no way to delegate to the next middleware 
component on the middleware-stack, right?

But there is - you just apply normal OOP and use dependency injection

class RouterMiddleware implements Middleware {
    /**
     * @var Router
     */
    private $router;

    /**
     * @var Middleware $next
     */
    private $next;

    public function __construct(Router $router, Middleware $next) {
        $this->router = $router;
        $this->next = $next;
    }

    public function dispatch(RequestInterface $request): ResponseInterface {
        if ($this->router->matches($request)) {
            return $this->router->handle($request);
        }
        return $this->next->process($request);
    }
}

The fact that this middleware may not always be able to process the request 
itself is reflected by it's required constructor argument for another 
middleware to potentially delegate to.

Some other middleware might always return a response, and isn't able to 
delegate at all - it's constructor signature will correctly reflect that 
fact:

class NotFoundMiddleware implements Middleware {
    public function __construct() {} // doesn't accept other middleware

    public function dispatch(RequestInterface $request): ResponseInterface {
        return new Response(...); // 404 not found
    }
}

The dependencies of each middleware is correctly expressed by the 
individual constructor of each middleware.

You compose a "middleware stack" not as a data-structure, but simply by 
proxying each middleware with another middleware:

$stack = new ErrorHandlerMiddleware(
    new CacheMiddleware(
        new RouterMiddleware(
            new Router(...),
            new NotFoundMiddleware()
        )
    )
);

This visually and logically reflects the "onion" structure that is often 
used to describe how middleware works, which is not apparent from the flat 
array-based structure used by current middleware dispatchers.

If you're not comfortable with the nested structure, you could of course 
arrange the code to make it look more like a stack as well, e.g. decorating 
a kernel layer by layer, from the inside-out:

$kernel = new NotFoundMiddleware();
$kernel = new RouterMiddleware(new Router(...), $kernel);
$kernel = new CacheMiddleware($kernel);
$kernel = new ErrorHandlerMiddleware();

You can't make this stack appear "upside down" the way it does with most 
existing middleware stacks - while that is visually appealing, because you 
can imagine the request coming in from the top and moving towards the 
bottom, that doesn't reflect what's really going on. It's the other way 
around - the inner-most middleware is a dependency of the next middleware 
out, and so on.

What you're building is an HTTP kernel, which is in fact not a stack, but a 
series of proxies or decorators - so the code is going to reflect the 
dependencies of the each component, rather than the flow of a request being 
processed.

Since there is no stack, no "runner" or "dispatcher" is required to 
dispatch the HTTP kernel at all:

    $response = $kernel->process($request);

In other words, no framework is required to implement the layer-like 
behavior that current middleware dispatchers "simulate" - the layered 
structure is inherent in the design, and the requirements of each layer, 
and whether or not it accepts a delegate, is formally defined by the 
constructor of each middleware component.

This also means you can't compose a middleware stack that might topple over 
- you won't be able to create such a stack at all, because the only way to 
construct a valid kernel out of middleware, will be to start with an 
inner-most middleware, such as a 404-middleware, that doesn't require any 
delegate middleware.

Likewise, you won't be able to compose a middleware stack with unreachable 
middleware components - putting a 404-middleware before any other 
middleware, for example, is impossible, since it's constructor doesn't 
accept a delegate middleware.

Any HTTP kernel you can compose is pratically guaranteed to be complete and 
meaningful, which isn't true of the traditional middleware architecture 
we've been discussing.

Some middleware components might even compose multiple other components, 
and delegate to them based on file-extension, domain-name, cache-headers, 
or anything else.

$stack = new PathFilterMiddleware(
    [
        "*.html" => new RouterMiddleware(...),
        "*.json" => new APIMiddleware(...),
    ],
    new NotFoundMiddleware()
);

No middleware "pipe" is required to compose a forked (tree) structure as in 
this example.

If I have to be completely honest, compared with anything we've done with 
PSR-15 or similar frameworks, I find that this is both simpler, easier to 
understand, more explicit, and far more flexible in every sense of the word.

The only thing I find perhaps not appealing about this, is the fact that 
all middleware needs to be constructed up-front - in PHP, that may be a 
problem.

However, in my experience, middleware is generally cheap to initialize, 
because it doesn't typically do anything at construction-time - it doesn't 
do anything, initialize or load any dependencies etc, until the process() 
method is invoked. And, most middleware stacks aren't actually very complex 
when it comes down to it - so this problem may be (at least in part) 
imagined.

There would be ways around that though, such as using a simple proxy 
middleware to defer creation:

class ProxyMiddleware implements Middleware {
    private $callback;
    public function __construct($callback) {
        $this->callback = $callback;
    }
    public function dispatch(RequestInterface $request): ResponseInterface {
        return call_user_func($this->callback, $request);
    }
}

This could proxy anything:

$stack = new ProxyMiddleware(function (RequestInterface $request) {
    $expensive = new ExpensiveMiddleware(...);
    return $expensive->process($request);
});

Or use a simple PSR-11 proxy to defer and delegate the actual bootstrapping 
of the middleware stacj to a DI container: 

class ContainerProxyMiddleware implements Middleware {
    private $container;
    private $id;
    public function __construct(ContainerInterface $container, $id) {
        $this->container = $container;
        $this->id = $id;
    }
    public function dispatch(RequestInterface $request): ResponseInterface {
        return $this->container->get($id)->process($request);
    }
}

Both approaches would let you defer the creation of any middleware 
component and their dependencies until first use.

Of course, in a long-running application, these concerns aren't even 
concerns in the first place - building the middleware stack can be done 
up-front without problems.

But even in a traditional setup with an "index.php" front-controller, the 
request overhead would typically consist of a few calls to mostly-empty 
constructors, so we're most likely talking microseconds (if any measurable) 
difference.

I know you will intuitively want to look for reasons to dismiss this idea, 
but all of this has to make you think?

As said, I have a substantial time-investment in PSR-15 myself, and I just 
spent two weeks trying to dismiss this idea myself.

Unfortunately I can't.

Trust me, I am *NOT* looking for reasons to shit on my own work, but the 
appeal of something much simpler, less error-prone, naturally type-safe, 
more flexible, which doesn't even require any framework at all... it's 
pretty hard to deny.

It has to make you think, right?

-- 
You received this message because you are subscribed to the Google Groups "PHP 
Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to php-fig+unsubscr...@googlegroups.com.
To post to this group, send email to php-fig@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/php-fig/674917bb-623a-4cea-b934-add8887acc65%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to