Laminas MVC Future

With a slight delay, but with positive news:

… Laminas MVC would continue with security-only status until the security support for PHP 8.4 ends, which will be 31st December 2028.

Thanks — having 31 Dec 2028 in writing genuinely helps planning, even if
it’s not the answer everyone was hoping for.

I’d like to pick up the point raised earlier, because we’re in exactly
that situation and I don’t think the thread ever got a concrete answer.

We run a fairly large API on Laminas API Tools (so MVC underneath): 183 REST services,
~500k handwritten LOC, PHP 8.4. Auth is api-tools-oauth2/bshaffer with 6 custom grant types plus
OpenID Connect. We already maintain 14 forks of api-tools and laminas packages just to
stay on PHP 8.4 — so “just fork it” isn’t a future option for us, it’s what we do today, and
it’s not what I want to still be doing in 2029.

We’ve mapped out the alternatives (keep forking, Mezzio, incremental Symfony, full rewrite),
but every effort number we have is a guess. What I can’t find
anywhere is a real data point, so:

  1. Has anyone actually migrated a large api-tools application to Mezzio? Not a greenfield
    Mezzio app — a migration. How many endpoints, how many people, and how long did it really
    take versus the original estimate?
  2. What did the api-tools-specific layers map onto? mezzio-hal looks like a close match
    for HAL, but I’d like to hear how content negotiation, api-tools-content-validation /
    input filter specs, api-tools-mvc-auth + ACL, and custom OAuth2 grant types actually
    landed in practice.
  3. @arhimede mentioned adopting Mezzio incrementally inside an existing MVC app — has
    anyone run that in production for a year or more? What did the boundary between old and new
    look like for routing, auth and sessions, and did it stay manageable?
  4. Did anyone evaluate Mezzio and decide to leave the Laminas ecosystem instead? I’m not
    looking to start a framework war — I’d just like to hear what tipped the decision. The
    long-term-guarantee concern @Jilco raised is the one I get asked about internally too:
    we’ve now lived through ZF → Apigility → API Tools → MVC retiring, and it’s hard to argue
    from the outside that the next hop won’t come.

If we do go ahead, I’m happy to write up whatever we learn. There’s still no migration guide
for api-tools users, and I suspect quite a few of
us are quietly sitting on this stack.

@babuka19901990 — I’ll take #3 since it was pointed at me, and add what I can on #1 and #2.

Where I’m speaking from: at Apidemia we’ve been on Expressive/Mezzio since 2017, and we maintain Dotkernel, a headless platform built on Mezzio. The relevant piece for you is Dotkernel API — a Mezzio + Doctrine ORM application that exists to do the job api-tools did, minus the config-driven layer. It’s public, so instead of taking my word on how the api-tools concerns map, you can read a working pipeline: GitHub - dotkernel/api: A production-ready REST API application built on the Mezzio microframework and Laminas components, which implements standards like PSR-3, PSR-4, PSR-7, PSR-11 and PSR-15. · GitHub

2. What the api-tools layers map onto

Answering this first, because it determines the effort in #1.

api-tools target
api-tools-hal mezzio/mezzio-hal
api-tools-content-negotiation pipeline middleware; in Dotkernel it’s config/autoload/content-negotiation.global.php, Accept/Content-Type → application/json, application/hal+json
ApiProblem mezzio/mezzio-problem-details (RFC 7807 / 9457)
api-tools-content-validation + input filter specs laminas/laminas-inputfilter — unchanged, specs port as-is
api-tools-mvc-auth + ACL mezzio/mezzio-authentication + mezzio-authorization-acl / -rbac
api-tools-oauth2 (bshaffer) mezzio/mezzio-authentication-oauth2league/oauth2-server
api-tools-rest / -rpc controllers PSR-15 handlers
api-tools-versioning route prefix or Accept-header middleware

Where it isn’t a clean swap:

  • HAL is genuinely close. The one gotcha — Doctrine proxies confusing metadata extraction — is fixed upstream now; we used to ship dotkernel/dot-doctrine-metadata as a wrapper for exactly that, and it’s in maintenance mode because the reason for it went away.
  • Input filter specs port nearly verbatim. Keep them in phase 1. Don’t do the typed-request/DTO refactor in the same commit as the framework move — you lose your ability to attribute a regression.
  • The OAuth2 layer is your actual risk, not the 183 endpoints. bshaffer and league are different servers, not two versions of the same one. Six custom grants means six rewrites against league’s GrantTypeInterface / AbstractGrant — each individually small and well isolated, but the client/token/scope schema differs, so it’s also a data migration, plus key material, plus a decision about tokens in flight at cutover. And league core is OAuth2 only; OIDC needs an extension layered on top. If I were scoping this I’d spike the grants and OIDC first and estimate everything else afterwards. Endpoint conversion is boring and linear once the auth boundary works.

1. Real migration numbers

We’ve done Apigility/api-tools → Mezzio migrations, but I’d rather not put someone else’s project figures in a public thread. What I can give you is the shape, which is where estimates usually go wrong:

  • Per-endpoint conversion is the cheap, predictable, parallelizable part. Estimates for it tend to be roughly right.
  • Estimates blow up on behaviour api-tools made implicit — collection rendering, field whitelists, ACL rules sitting in config nobody has read since 2016. Budget for characterisation tests before conversion, not after. If you can’t currently pin all 183 services with request/response fixtures, that’s work item zero, and it pays for itself even if you decide to stay on forks.
  • 500k LOC is a misleading number. Most of it isn’t framework-coupled. Measure what actually touches AbstractResourceListener, the event manager, controller plugins and the api-tools config tree. That figure is your migration; the rest is code that moves buildings without noticing.

3. Mezzio incrementally inside MVC, in production, over a year+

Yes, we run this, and it stays manageable if the boundary is drawn in one place. What works:

  • Mezzio outermost. public/index.php runs the Mezzio pipeline; the last middleware in it is a legacy bridge that hands off to Laminas\Mvc\Application and buffers the result into a PSR-7 response. Anything routed in Mezzio wins, everything else falls through untouched. The reverse — dispatching Mezzio from an MVC route — also works, but leaves you with two routers arguing over precedence.
  • One container. Both sides are laminas-servicemanager, so it’s one PSR-11 container, one config, one EntityManager, one connection pool. This is the concrete argument for doing the incremental move inside the ecosystem: an incremental Symfony migration gives you two containers and two service graphs for the entire duration, and that duration is measured in years.
  • Authenticate once, at the boundary. Do it in the Mezzio pipeline, put the identity on a request attribute, and give the legacy side a thin shim that reads that attribute instead of api-tools-mvc-auth. Two live auth stacks is where these projects become unmanageable.
  • Sessions are mostly a non-issue for you, and it’s an advantage of an API-tools stack: bearer tokens are stateless and cross the boundary for free. (For session-based MVC frontends you need a SessionPersistenceInterface that reads laminas-session’s storage so both sides see the same session. Doable, but it’s the fiddly part.)
  • Direction rule: new code may call legacy services; legacy code must never call new handlers. Enforce it in review. That single rule is what keeps a strangler from turning into a distributed monolith.

What degrades it: no forcing function. Cut over per resource with a date per resource. “We’ll migrate things as we touch them” is how you end up running both stacks in 2029, which is the outcome you said you want to avoid.

4. On leaving the ecosystem

I’m not going to argue you out of evaluating Symfony — with your constraints you should. But I’d separate the question, because “will this framework exist in 2035” is the wrong thing to optimise for.

In Mezzio, the surface you depend on is PSR-7, PSR-11, PSR-15. A handler is a class with one method that takes a request and returns a response. If Mezzio vanished tomorrow, those handlers run on any other PSR-15 runner with a new config layer — days of work, not a rewrite. That’s a structural property, not a promise, and it’s the honest answer to @Jilco’s concern. ZF → Apigility → API Tools hurt precisely because the coupling was deep and config-driven; the whole point of the middleware model is that it isn’t.

On maintenance specifically: Apidemia funds work on Mezzio and the surrounding packages because our own product line sits on it — Dotkernel API shipped v7 in December with PostgreSQL, native UUID and PHP 8.5 support. That’s a commercial interest rather than a guarantee, and you should weigh it as such. But it’s a different risk profile from “volunteers ran out of time.”

Happy to go into detail on the grant-type rewrite or the legacy bridge if that’s useful. And yes to the write-up — if you document your migration, we’ll contribute the api-tools → Mezzio mapping from our side. That guide should exist and it doesn’t.