For over a year, since 2.7, we have had the ability to use a single middleware in a Zend\Mvc-based application (if you're not familiar, that's basically "Zend Framework" as opposed to "Zend Expressive"). A nice advantage here is you can make a forward-port path to migrating across to Zend Expressive. Your config might look something like

<?php
declare(strict_types=1);

return [
    'router' => [
        'routes' => [
            'path' => [
                'type' => 'Literal',
                'options' => [
                    'route' => '/path',
                    'defaults' => [
                        'middleware' => \App\Actions\FooAction::class,
                    ],
                ],
            ],
        ],
    ],
];

The middleware FooAction would be a standard middleware (at that time, the http-interop style middlewares were not supported, but that's changed now as I'll explain):

<?php
declare(strict_types=1);

namespace App\Actions;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\JsonResponse;

final class FooAction
{
    public function __invoke(RequestInterface $request, ResponseInterface $response, callable $next = null) {
        return new JsonResponse([
            // ... some data ...
        ]);
    }
}

This change was great, and I did some work for a client who had started using the MiddlewareListener to attach a middleware to a route like this, and saw it in action. Over time I discovered a couple of flaws. Firstly, the matched route information was unavailable, so if you had a route matcher like /foo/:slug, there was no nice way to discover the value of slug from the matched route. So, dutifully I created patch #210 to resolve this issue, which was accepted and released in Zend\Mvc 3.0.4 and up.

Before long, I discovered another glaringly obvious problem: you could only ever have a single middleware added here. If you're familiar with middleware-style applications, this completely defeats the point: you can pipe your middleware to inject behaviour (such as authentication, error handling etc.) but this was not possible.

So, leveraging the existing functionality of Zend\Stratigility pipes, I heavily refactored the MiddlewareListener to instead create a pipe from the middleware definition from config. I made it backwards compatible in the sense that the configuration above would still work, but now you can attach multiple middlewares to a route, like so:

<?php
declare(strict_types=1);

return [
    'router' => [
        'routes' => [
            'path' => [
                'type' => 'Literal',
                'options' => [
                    'route' => '/path',
                    'defaults' => [
                        'middleware' => [
                            \App\Middleware\ExtractJsonPayload::class,
                            \App\Middleware\StoragelessPsr7Session::class,
                            \App\Middleware\VerifyIdentity::class,
                            \App\Actions\FooAction::class,
                        ],
                    ],
                ],
            ],
        ],
    ],
];

This patch recently got merged in Zend\Mvc 3.1.0 so you can now take advantage of this handy new feature. I also added support for middlewares that implement Interop\Http\ServerMiddleware\MiddlewareInterface too, so you can now write something like:

<?php
declare(strict_types=1);

namespace App\Actions;

use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;

final class FooAction implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        return new JsonResponse([
            // ... some data ...
        ]);
    }
}

I provide development, consulting and training for Zend Expressive, Zend Framework, Doctrine and more. If you'd like to discuss your requirements, get in touch with me.