If you learned PHP web development through Zend Framework, Mezzio can feel alien at first glance. There are no controllers. There are no action methods. There is no dispatch event. The module system is gone. The view model object is gone. It looks like someone stripped out everything you know and replaced it with interfaces you have never seen.
But underneath the unfamiliar surface, the patterns you already understand still apply. A Mezzio request handler does the same job as a controller action. Middleware does the same job as controller plugins and event listeners. The pipeline is just the dispatch cycle made explicit. Once you see the mapping, Mezzio stops being foreign and starts being familiar-but-simpler.
The Mental Model Shift
In Zend Framework MVC, a request flows through a series of hidden steps: the front controller receives the request, the router matches a route, the dispatch event fires, a controller is instantiated, an action method runs, a view model is returned, the renderer turns it into HTML, and the response goes out. Most of this happens behind event listeners that you configure but rarely see execute.
In Mezzio, all of that collapses into a middleware pipeline. The request enters at the top, passes through each middleware in sequence, and a response comes back. There is no hidden event system. The pipeline is a flat list, and you can read it from top to bottom to understand exactly what happens for every request.
1 | // Mezzio pipeline - everything is visible |
Compare this to the Zend MVC event manager where you might have listeners on MvcEvent::EVENT_ROUTE, MvcEvent::EVENT_DISPATCH, MvcEvent::EVENT_RENDER, and MvcEvent::EVENT_FINISH, each registered at different priorities, some in module bootstrap methods and some in configuration. The Mezzio pipeline does the same work with less machinery.
Controllers Become Request Handlers
A Zend Framework controller action looks like this:
1 | class BlogController extends AbstractActionController |
The equivalent Mezzio handler looks like this:
1 | class BlogListHandler implements RequestHandlerInterface |
The differences are cosmetic:
- The handler implements
RequestHandlerInterfaceinstead of extendingAbstractActionController - Dependencies are constructor-injected instead of accessed through
$this->getServiceLocator()or controller plugin methods - The handler returns a PSR-7
ResponseInterfaceinstead of aViewModel - Template rendering is explicit instead of implied by the return value
The business logic is identical. You still fetch posts and pass them to a template. The surrounding ceremony changes, not the substance.
Controller Plugins Become Middleware
Zend MVC controller plugins are one of the framework’s most distinctive features. $this->redirect(), $this->params(), $this->identity(), $this->flashMessenger(), these are all plugins injected into the controller via the plugin manager.
In Mezzio, these become middleware or request attributes:
| ZF MVC Plugin | Mezzio Equivalent |
|---|---|
$this->redirect()->toRoute(...) |
return new RedirectResponse($this->router->generateUri(...)) |
$this->params()->fromRoute('id') |
$request->getAttribute('id') |
$this->params()->fromQuery('page') |
$request->getQueryParams()['page'] ?? 1 |
$this->identity() |
$request->getAttribute(UserInterface::class) (set by auth middleware) |
$this->flashMessenger() |
Flash message middleware that reads/writes session |
$this->layout() |
Template layout configuration |
The pattern is consistent: things that MVC made available through magic methods on the controller are made available through the request object or explicit dependencies in Mezzio.
Routing Is Configuration, Not Convention
Zend MVC uses a complex routing tree with literal, segment, regex, and wildcard route types. A typical route definition:
1 | 'router' => [ |
Mezzio routing with FastRoute is flatter and more explicit:
1 | $app->get('/blog', BlogListHandler::class, 'blog.list'); |
Each route maps to exactly one handler. There is no :action parameter that dispatches to different methods on the same controller. This feels like a loss of convenience at first, but it means every route has a dedicated, testable handler class. When something goes wrong at /blog/42, you know exactly which class to look at.
The Service Container Works the Same Way
If you are using laminas-servicemanager (and most Zend/Laminas applications are), the container in Mezzio is the same component. Factories, abstract factories, delegators, aliases, and invokables all work identically.
1 | // This factory works in both MVC and Mezzio |
The main difference is that MVC has a separate ControllerManager with its own factory resolution, while Mezzio uses the main container for everything. This simplification means one less layer of indirection.
View Rendering Is Explicit
In Zend MVC, returning a ViewModel from a controller action triggers an implicit rendering pipeline. The framework resolves the template name from the controller and action names, renders the template, injects it into the layout, and sends the response.
Mezzio makes each of those steps explicit:
1 | // You choose the template |
If you want JSON instead of HTML:
1 | return new JsonResponse($data); |
If you want no body at all:
1 | return new EmptyResponse(204); |
There is no view model object, no renderer event, no layout injection step. You render a template and put the result in a response. The output is entirely under your control.
For Zend developers who relied heavily on view helpers, the transition requires some adjustment. Most Laminas view helpers work with the Laminas View renderer in Mezzio. Custom view helpers need to be registered in the view helper manager, just as they were in MVC.
Event Listeners Become Middleware
The MVC event system is powerful but opaque. A common pattern is to register a listener on the dispatch event that checks authentication:
1 | // In Module.php |
In Mezzio, this becomes middleware in the pipeline:
1 | class AuthenticationMiddleware implements MiddlewareInterface |
The middleware either short-circuits the pipeline (by returning a redirect) or passes the request forward to the next handler with the user attached as a request attribute. The control flow is explicit.
What You Gain
The middleware model gives you several practical advantages over the MVC event system:
Testability. Each middleware class is a standalone unit with a clear interface. You can test it with a mock request and a mock handler without bootstrapping the entire application.
Readability. The pipeline definition is a flat list. New developers can read it and understand the request flow in minutes. The MVC event system requires reading module configuration, bootstrap methods, and listener registrations spread across multiple files.
Composability. You can compose middleware stacks for specific routes. Admin routes get authorisation middleware. API routes get CORS middleware. Public routes get caching middleware. Each route gets exactly the middleware it needs.
No priority numbers. MVC event listeners have priority numbers that determine execution order. Middleware runs in the order you pipe it. No more debugging why a listener at priority 100 runs before or after a listener at priority 200.
What You Lose
Honesty about trade-offs matters. The MVC model has some conveniences that Mezzio does not replicate:
Convention-based routing. MVC can map URLs to controllers automatically through naming conventions. Mezzio requires explicit route definitions for every endpoint.
Action methods on a single controller. In MVC, BlogController can have indexAction, viewAction, createAction, and deleteAction. In Mezzio, each of those is a separate handler class. This means more files, though each file is simpler.
Built-in form handling workflow. MVC’s form-controller integration with PRG (Post-Redirect-Get) support is convenient. In Mezzio, you manage the PRG flow yourself.
Module discovery. MVC modules self-register through configuration. Mezzio uses ConfigProvider classes that you explicitly wire into the application.
Getting Started
If you want to try Mezzio without migrating your existing application, create a fresh project:
1 | composer create-project mezzio/mezzio-skeleton mezzio-sandbox |
Build a simple handler that does what one of your existing controller actions does. See how the pieces connect. Once the mapping from MVC patterns to middleware patterns clicks, the migration path becomes straightforward.
For a detailed migration plan, the From Laminas MVC to Mezzio migration blueprint covers the parallel-running architecture. For the broader modernisation context, the Modernising Zend Framework Applications guide covers the incremental approach that applies to any framework migration.
The Request Lifecycle Explained for PHP Developers article covers the HTTP fundamentals that both MVC and middleware frameworks are built on.