I'm late to the thread (been a busy few weeks), but here goes...

The chief problem I see with this approach is the same problem I've observed in
Rack and Stack; namely: it's a convention-based approach, versus a
fuctional one.

In Rack, you use a specially named property (app), and assign it the application
and/or "next middleware" instance. As it's a _convention_, there's no way to
ensure it's populated, nor that it's populated with what's expected.

In Stack, you pass that value as the first constructor argument. My
understanding is that even if you will not be invoking it, you _must_ pass it
this way, or else it's not compatible. It also means that in any given
middleware class, the name of the property holding this value might be
different.

The reason Connect and ExpressJS started passing `next` around was precisely to
circumvent the issue of convention. By making it a direct requirement when
invoking middleware, any middleware has immediate and direct access to it.
There's no guesswork involved. (I'll note that it suffers from the same problem
as Stack, in that if you're not using it, it's a useless argument; however,
it has the benefit that the constructor is then used strictly for
_dependencies_, and not _runtime_.)

You noted another issue with injecting via the constructor, and Beau noted this
already: autowiring. How would a container know _which_ middleware to inject at
any given point? It _could_, if you use concrete class names, but that goes
against the whole point of middleware, which is _composability_. If you typehint
on the interface, though, you then need to go to some effort to ensure
containers can pull the middleware correctly.

The answer to that, according to your write-up, is that middleware in this
particular case forms an HTTP kernel for your application, and, as such, going
to the effort of creating the workflow is minimal. That argument breaks apart,
however, when you want to start re-using middleware as part of route-specific
workflows.

Just so everyone is following, this is what I mean by a route-specific workflow:

```php
$app->route('/api/blog', [
    Authentication::class,
    Authorization::class,
    Blog::class,
], ['GET', 'POST']);
```

In the above, the middleware piped into the application is a stack itself. Doing
this allows me to re-use things like authentication and authorization, but also
compose them such that they only apply to specific routes.

Following from the above example, I might write authentication middleware. This
_could_ be used for the entire application, but it might be better to only put
it in front of routes that actually require authentication. If we go the
constructor injection path, now I have to apply that to my _routed_ middleware
as well, which likely has me instantiating my entire object graph for my entire
application on each and every request, and creating _duplicate_ instances of
common middleware (due to the need to compose different _nested_ middleware
depending on the route) — a problem that's been solved by using DI containers as
part of dispatchers for many years now. Is that expensive? It can be. It's less
expensive under PHP 7 than it was in PHP 5, but if we can avoid instantiating
something unless we actually use it, why shouldn't we? If we can avoid multiple
instances of the same class due to different inner middleware, shouldn't we?
Particularly if it's _easier_ (e.g., using a class name in the above examples,
instead of instantiating objects and providing dependencies manually).  Beau
does a nice job of illustrating this in his most recent post to this thread.

In the above example, authentication and authorization are not _controllers_,
they are _middleware_; the idea is that they would return a response if
conditions fail, but otherwise delegate deeper into the stack. But they are
being composed as part of _routed_ middleware, which you would term
"controllers", in order to have HTTP workflows that are specific to a given
route.

Now, does innermost middleware need to receive the delegate? No, but by having a
single interface to describe any link in the chain, versus two interfaces (one
for middleware that delegates, one for the final dispatched "things" --
controller, action, what have you), we simplify how application stacks/kernels
are built, as everything can be injected exactly the same.

The power of this is seen, again, in the above example. I can route these final
handlers that are guaranteed to return a response in the same way I might route
any middleware, and that allows me to in turn route a stack/pipeline in place of
such final handlers. Without that ability, the story becomes far more
complicated - these stacks then either need to open up what the accept (in other
words, _remove typehints_), or have multiple methods for routing things into
them, and then also worry about how to handle stacks versus final middleware.

My argument is that your proposal may be simpler in _some_ contexts, but loses a
ton of flexibility and power outside of those contexts. The proposed PSR-15 as
it currently stands addresses these issues, and does so in a way that does not
require specific conventions.


On Fri, Apr 21, 2017 at 10:42 AM, Rasmus Schultz <[email protected]> wrote:
> 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 [email protected].
> To post to this group, send email to [email protected].
> 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.



-- 
Matthew Weier O'Phinney
[email protected]
https://mwop.net/

-- 
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 [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/php-fig/CAJp_myWNHthoiYbcmCb51Dh%3DC7fpqaaHYSA8yJPijbU88xtx5g%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to