Cookbook
Using Double-Pass Middleware
Mezzio uses PSR-15 middleware and request handlers exclusively as of version 3.
In previous releases, however, we supported "double-pass" middleware, and a number of third-party packages provided double-pass middleware. How can you use this middleware with Mezzio 3?
What is Double-Pass Middleware?
Double pass middleware receives both the request and a response in addition to the handler, and passes both the request and response to the handler when invoking it:
function (ServerRequestInterface $request, ResponseInterface $response, callable $next) { $response = $next($request, $response); return $response->withHeader('X-Test', time()); }
It is termed "double pass" because you pass both the request and response when delegating to the next layer.
doublePassMiddleware function
laminas-stratigility v2.2 and v3.0 ship a utility function,
Laminas\Stratigility\doublePassMiddleware()
, that will decorate a callable
double-pass middleware using a Laminas\Stratigility\Middleware\DoublePassMiddlewareDecorator
instance; this latter is a PSR-15 implementation, and can thus be used in your
middleware pipelines.
The function (and class) also expects a PSR-7
ResponseInterface
instance as a second argument; this is then passed as the
$response
argument to the double-pass middleware. The following examples
demonstrate both piping and routing to double pass middleware using this
technique, and using laminas-diactoros to provide the response instance.
use Laminas\Diactoros\Response;
use function Laminas\Stratigility\doublePassMiddleware;
$app->pipe(doublePassMiddleware(function ($request, $response, $next) {
$response = $next($request, $response);
return $response->withHeader('X-Clacks-Overhead', 'GNU Terry Pratchett');
}, new Response())); // <-- note the response
$app->get('/api/ping', doublePassMiddleware(function ($request, $response, $next) {
return new Response\JsonResponse([
'ack' => time(),
]);
}, new Response())); // <-- note the response
Double-Pass Middleware Services
What if you're piping or routing to a service — for instance, a class provided by a third-party implementation?
In this case, you have one of two options:
- Decorate the middleware before returning it from the factory that creates it.
- Use a delegator factory to decorate the middleware.
Decorating via factory
If you have control of the factory that creates the double-pass middleware you will be using in your application, you can use the strategy outlined above to decorate your middleware before returning it, with one minor change: you can pull a response factory from the container as well.
To demonstrate:
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use function Laminas\Stratigility\doublePassMiddleware;
class SomeDoublePassMiddlewareFactory
{
public function __invoke(ContainerInterface $container)
{
// Create the middleware instance somehow. This example
// assumes it is in `$middleware` when done.
return doublePassMiddleware(
$middleware,
($container->get(ResponseInterface::class))()
);
}
}
That last line may look a little strange.
The Psr\Http\Response\ResponseInterface
service returns a callable factory
for producing response instances, and not a response instance itself. As such,
we pull it, and then invoke it to produce the response instance for our
double-pass middleware.
This approach will work, but it means code duplication everywhere you have double-pass middleware. Let's look at the delegator factory solution.
Decorating via delegator factory
Delegator factories can be re-used for multiple services. In our case, we'll re-use it to decorate double-pass middleware.
The delegator factory would look like this:
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use function Laminas\Stratigility\doublePassMiddleware;
class DoublePassMiddlewareDelegator
{
public function __invoke(Container $container, string $serviceName, callable $callback)
{
return doublePassMiddleware(
$callback(),
($container->get(ResponseInterface::class))()
);
}
}
This looks similar to our previous solution, but is self-contained; we rely on
the $callback
argument to produce the middleware we want to decorate.
Then, for each service we have that represents double-pass middleware, we can provide configuration like the following:
return [
'dependencies' => [
'delegators' => [
SomeDoublePassMiddleware::class => [
DoublePassMiddlewareDelegator::class,
],
],
],
];
This approach has a couple of benefits:
- We do not need to change existing factories.
- We do not need to extend factories from third-party services.
- We can see explicitly in our configuration all services we consume that are double-pass middleware. This will help us identify projects we want to contribute PSR-15 patches to, or potentially migrate away from, or middleware of our own we need to refactor.
Extending the MiddlewareContainer
Another possibility is to extend Mezzio\MiddlewareContainer
to add
awareness of double-pass middleware, and have it auto-decorate them for you.
A contributor has created such a library:
- https://github.com/Moln/expressive-callable-middleware-compat
You can install it using composer require moln/mezzio-callable-middleware-compat
.
Once installed, add its Moln\MezzioCallableCompat\ConfigProvider
as an
entry in your config/config.php
after the Mezzio\ConfigProvider
entry. This last point is particularly important: providers are merged in the order
presented, with later entries having precedence; you need to ensure the new
package overrides the MiddlewareContainer
service provided by mezzio!
When you use this approach, it will automatically detect double-pass middleware and decorate it for you.
The main drawback with such an approach is that it will not help you identify double-pass middleware in your system.