Moving from Laminas MVC to Mezzio is not a weekend project. It is a controlled migration that takes weeks or months depending on the size of your application. The good news is that you do not need to stop shipping features while you do it, and you do not need to finish before you see value from the work.
This guide covers the practical blueprint: how to run both stacks in parallel, how to share services between them, how to migrate individual routes from MVC controllers to PSR-15 middleware handlers, and how to eventually remove the MVC layer entirely. It assumes you have already completed the namespace migration from Zend to Laminas (see the Zend to Laminas Migration Checklist if you have not).
If you are still deciding whether Mezzio is the right target, the Laminas MVC End of Life in 2026 decision guide covers the trade-offs between freezing, strangling, and component replacement.
Why Mezzio Rather Than a Different Framework
Mezzio is the natural migration target for Laminas MVC applications because it shares the same component ecosystem. Your laminas-db table gateways, your laminas-form instances, your laminas-authentication adapters, they all work inside Mezzio without modification. The framework layer changes but the components do not.
The PSR-15 middleware standard that Mezzio implements is also the direction the broader PHP ecosystem has moved. Slim 4, Laravel’s middleware pipeline, and Symfony’s HTTP kernel all converge on the same request/response model. Learning Mezzio’s pipeline is transferable knowledge.
The practical advantage is that you can share your service container between the MVC application and the Mezzio application during the transition. Services registered in one are available in the other. This means you do not need to duplicate your database connections, cache clients, or authentication services.
Setting Up the Parallel Architecture
The parallel running strategy uses a front-controller approach. A single entry point inspects the incoming request and decides whether to route it to the Mezzio pipeline or fall through to the Laminas MVC application.
Step 1: Install Mezzio Alongside MVC
Add the Mezzio packages to your existing Composer project:
1 | composer require mezzio/mezzio mezzio/mezzio-fastroute mezzio/mezzio-laminasviewrenderer |
You do not need a separate Composer project. Mezzio and Laminas MVC coexist in the same vendor directory.
Step 2: Create the Mezzio Configuration
Create a config/mezzio/ directory alongside your existing MVC configuration. The key files are:
1 | config/ |
The container configuration should extend your existing MVC service manager rather than replacing it. This is the critical piece that enables shared services:
1 | // config/mezzio/container.php |
Step 3: Create the Routing Split
In your public/index.php, add the routing decision before the MVC application boots:
1 | $mezzioRoutes = require __DIR__ . '/../config/mezzio/routes.php'; |
This is intentionally simple. The routing decision is a string prefix match, not a full regex evaluation. You want this check to be fast and obvious.
Migrating Your First Route
Pick a route that meets these criteria for your first migration:
- It is a GET endpoint with no complex form handling
- It does not depend on MVC-specific view helpers extensively
- It has existing tests (or you can write characterisation tests for it quickly)
- It is not your most critical revenue path
A typical candidate is an API endpoint, a status page, a simple read-only listing, or a report view.
Writing the Handler
A Mezzio handler is a PSR-15 request handler. It receives a PSR-7 ServerRequest and returns a PSR-7 Response:
1 | namespace App\Handler; |
Notice that the handler depends on the same StatusChecker service your MVC controller used. Because you share the service container, this just works.
Registering the Route
In config/mezzio/routes.php:
1 | return [ |
Handling Templates
If your MVC application uses Laminas View, Mezzio can use the same template renderer. The mezzio/mezzio-laminasviewrenderer package provides a PSR-compatible adapter around Laminas View. Your existing .phtml templates work without modification.
The only change is the template path resolution. In MVC, templates resolve through a module-based path stack. In Mezzio, you configure template paths explicitly:
1 | 'templates' => [ |
You can point these paths at your existing MVC view directories during the transition. Once a route is fully migrated, move its templates to the Mezzio template directory.
Sharing Services Without Duplication
The service container is the bridge between your two stacks. Here are the services you almost certainly need to share:
- Database connections (laminas-db adapter or Doctrine DBAL)
- Authentication service (laminas-authentication or a custom adapter)
- Session manager (critical for maintaining logged-in state across both stacks)
- Cache client (Redis, Memcached, or filesystem cache)
- Logger (PSR-3 logger instance)
- Configuration array (merged config from all sources)
Register these in your shared service manager configuration and access them by the same service name in both MVC controllers and Mezzio handlers.
The one gotcha is that Laminas MVC’s ControllerManager is separate from the main ServiceManager. If your MVC controllers have dependencies injected through controller factories, those factories reference the ControllerManager, not the shared container. When migrating those controllers to Mezzio handlers, their factory code simplifies because Mezzio uses a single container.
Migrating POST Endpoints and Forms
POST endpoints are harder than GET endpoints because they involve input validation, CSRF protection, form state, and error redisplay. If you are using laminas-form, the migration path is:
- Keep using laminas-form in your Mezzio handlers. The form objects are framework-independent.
- Move CSRF validation from the MVC CSRF view helper to a dedicated middleware that runs before your handler.
- Handle form validation in the handler and return either a redirect (on success) or a re-rendered template with error messages (on failure).
1 | public function handle(ServerRequestInterface $request): ResponseInterface |
This is essentially the same flow as your MVC controller action, but expressed as a PSR-15 handler. The mental model does not change much.
Middleware Pipeline Patterns
One of Mezzio’s strengths over MVC is the explicit middleware pipeline. Instead of event listeners, plugins, and controller plugins scattered across your MVC application, you compose middleware in a visible sequence:
1 | $app->pipe(ErrorHandler::class); |
Each middleware does one thing. The pipeline reads top to bottom. There are no hidden event triggers.
For routes that need authorisation:
1 | $app->route('/admin/dashboard', [ |
The middleware array runs in sequence. If AuthorisationMiddleware returns a 403, the handler never executes.
Production Cutover Strategy
Do not migrate all routes and then flip a switch. Instead:
- Migrate one route. Deploy it. Monitor it for a week.
- Migrate a batch of related routes. Deploy. Monitor.
- Migrate the highest-traffic routes. These need extra monitoring and rollback readiness.
- Migrate the last MVC routes. At this point, you know the Mezzio stack handles your production load.
- Remove the MVC bootstrap and routing split. Your
public/index.phpnow boots Mezzio directly.
Each step is a deployable, reversible change. If a migrated route has issues, you revert the routing split to send that path back to MVC while you debug.
Common Mistakes
Trying to migrate the entire application in one branch. This creates a merge nightmare and removes your ability to deploy incrementally. Migrate one route per PR.
Duplicating service factories. If you catch yourself copying service factories from MVC configuration to Mezzio configuration, stop. Share the container.
Rewriting business logic during migration. The migration is a framework change, not a refactoring opportunity. Move the code first, refactor later. Mixing both doubles your risk.
Forgetting session handling. MVC and Mezzio need to share the same session store and session ID. If they use different session configurations, users will appear logged out when they hit a Mezzio route.
Ignoring the middleware order. In MVC, the event manager fires things in a less visible sequence. In Mezzio, order is explicit and mistakes are silent. Test your pipeline order with integration tests.
FAQ
Can I use a reverse proxy instead of a front-controller split?
Yes. Nginx or Apache can route specific URL prefixes to a separate Mezzio application running on a different port or socket. This is more complex to set up but provides better isolation.
What about view helpers?
MVC view helpers do not work directly in Mezzio templates. You will need to replace them with Mezzio-compatible helpers or plain PHP function calls. Common ones like URL generation and CSRF tokens have Mezzio equivalents.
How do I handle MVC event listeners?
Convert them to middleware. An MVC dispatch listener that checks permissions becomes an authorisation middleware. A render listener that injects template variables becomes a middleware that adds request attributes.
What if my MVC application uses modules extensively?
Mezzio does not have a module system in the same sense. Module configuration is replaced by ConfigProvider classes that return arrays. The migration is mechanical: extract each module’s config into a ConfigProvider and register it in the Mezzio container.
Next Steps
Start by installing Mezzio alongside your existing application and creating the routing split. Pick your first route, migrate it, and deploy. The architectural pattern is covered in more depth in the Modernising Zend Framework Applications guide, and the component-level details of the Zend-to-Laminas transition are in the Zend to Laminas Migration Checklist.
For understanding why the MVC dispatch cycle works the way it does, and why the middleware model is architecturally cleaner, the Application Architecture chapter explains the front controller and dispatch patterns that both frameworks are built on.