I've been busy lately with trying to shepherd the last ZF3 initiatives, so many
apologies for being late to the game on the Middleware/PSR-7 topics. Things are
starting to slow down, so I set aside today to go through the various threads
and comb through the proposals in detail. What follows is lengthy; for that
reason, I created a "tl;dr" at the end. If you want the "why" behind those
bullet points, keep reading.
The following comments are on the PSR-15 proposal and metadocument.
## Proposal document
### Section 1
Why type-hint against callable?
Middleware stacks can always allow callable middleware, *and then wrap it in an
implementation of the interface*. Anthony demonstrated it in his blog post, and
I've done similarly already when wrapping middleware of different signatures.
The problem I see with allowing any callable is type safety. While it's
admittedly rare, I've seen cases where a callable with an incorrect argument
order or declarations causes problems. Having this strictly typed solves this.
Additionally, most of the implementations I've seen *do* define an interface
already, even if they do not use it. If middleware implements these, wrapping
them becomes fairly trivial when accepting them to the pipeline:
```php
if ($middleware instanceof SomeSupportedThirdPartyMiddlewareInterface) {
$middleware = new
WrapperMiddlewareForSomeSupportedThirdPartyMiddlewareInterface($middleware);
}
```
### Section 2
Two big questions:
1. **Why __invoke()?**
One issue with specifying `__invoke()` in the interface at this point is if
an implementing library defined its own interface, if there are *any*
differences in the signature (e.g., if `$next` is nullable; I know, I f@!#ed
that up in Stratigility, but ç'est la vie), then those implementations cannot
extend the proposed interface.
If we use a concrete method name, current implementations can extend it, and
provide wrappers that will decorate invokable objects with an implementing
method that proxies to their own `__invoke()`. Additionally, existing
dispatch libraries could easily test for the proposal interface vs callables
in order to determine how to dispatch. *This would help adoption
immediately, as libraries could likely add support within a minor version,
instead of a backwards-incompatible major version.*
2. **Why no interface for $next?**
Again, in the interest of type safety, this seems necessary. Additionally,
having such an interface define a concrete, non-magic method would allow for
immediate interoperability. As an example, Stratigility could implement the
new method as a proxy to its own `__invoke()` (or vice versa), preventing
conflicts in signatures.
### Section 2.2
I fully disagree with having the same interface for both client- and server-side
middleware.
Server-side middleware should be able to assume that the passed request is a
`ServerRequestInterface`. Having to test for capabilities within each middleware
will be a nightmare to explain to newcomers, and, frankly, impossible to
rationalize. If users just *assume* that they have received a
`ServerRequestInterface`, and get a plain `RequestInterface`, they'll have some
ugly surprises during execution.
## Metadocument
### Section 4
The reason ExpressJS passes the response is because there's no easy way to
create a new one in node, because the instance ties into the HTTP server's
event system. So, while we can use it as a justification, *the context is
different*.
Yes, I did port the exact signature when creating phly/conduit /
zendframework/zend-stratigility; I was originally doing a literal port, and,
even after switching PSR-7 to immutable instances, found the idea of passing the
response interesting in terms of eliminating dependencies in consuming
middleware.
However, I think there are some compelling reasons to drop the response
argument.
Most cases I've seen where a modified response was passed to a lower layer via
`$next()` have a fair amount of brittleness:
- You cannot assume the response returned is the same response or contains the
same modifications.
- Often, the modifications you need to do *should* be propagated, regardless of
the inner layers dispatched, or
- Modifications should be conditional based on the state of the response
returned from inner layers.
There are ways to rewrite the various content negotiation and caching examples
I've seen on the list to work in the lambda style, and these approaches tend to
be more robust and ensure the validity of the response.
Additionally, there's a learning curve to passing it in. As Rasmus Schulz noted,
What doesn't seem logical, is manipulating output (the response) on the way
in - in terms of control, it doesn't make any sense, because in doing so,
you don't really have any control at all, unless the next component on the
stack is accepting input from the response, which isn't intuitive or easy to
explain at all.
With the HTTP factories proposal (PSR-16) gaining traction (essentially, unless
the rest of FIG members who haven't voted vote now and vote against, or a lot of
folks change their votes), I see less and less reason to keep the argument.
Moreover, it would be reasonably easy to adapt existing middleware libraries to
the lambda style:
```php
/**
* Assumes MiddlewareInterface is now lambda-style
*/
class DoublePassToLambdaMiddleware implements MiddlewareInterface
{
private $middleware;
private $response;
/**
* Accepts the double-pass middleware, and a blank response
*/
public function __construct($middleware, $response)
{
$this->middleware = $middleware;
$this->response = $response;
}
/**
* Assumes that the interface uses a method other than __invoke.
* Just spit-balling here, and chose something not widely used.
*/
public function respond(ServerRequestInterface $request,
MiddlewareStack $next)
{
$middleware = $this->middleware;
return $middleware(
$request,
$this->response,
function ($request, $response) use ($next) {
return $next->respond($request, $response);
}
);
}
}
```
Existing libraries could easily wrap existing middleware in something like the
above to adapt it to PSR-15.
## Final notes
**tl;dr**: I've read through all the threads and had a thorough look at both the
PSR-15 proposal and metadocument, as well as the factories proposal (PSR-16). My
take is:
- The factories largely remove the requirement for passing the response when
invoking middleware.
- The brittleness of design that occurs when pre-modifying a response to pass
when invoking middleware convinces me the argument should be removed.
- I definitely feel separate interfaces are required for client- vs. server-side
middleware. It makes the signatures self-enforcing at the engine-level, makes
the arguments self-documenting, and prevents the need to type-check within
middleware when writing server-side middleware.
- I'd like to see a method other than `__invoke()` in the `MiddlewareInterface`
as it will make adapting existing libraries to be PSR-15 compliant easier.
- I'd like to see an interface for the `$next` argument, also with a method
other than `__invoke()`; again, this will be to allow adapting existing
libraries to PSR-15.
--
Matthew Weier O'Phinney
mweiero...@gmail.com
https://mwop.net/