Laminas, Mezzio, and Composable PSR-Oriented PHP Ecosystems
Understand what Laminas is and how its library-first philosophy differs from full-stack frameworks like Symfony and Laravel, recognize PSR-15 middleware and PSR-7 HTTP messages in a Laminas/Mezzio context, and understand the broader idea of assembling a PHP stack from interoperable PSR-compliant packages rather than adopting one opinionated framework.
Not every serious PHP application is built on Laravel or Symfony. A significant and long-running strand of the PHP ecosystem favors composing an application from small, independently-useful, standards-compliant libraries rather than adopting one framework's full opinionated stack — and Laminas is the most prominent example of a project explicitly built around that philosophy. Understanding Laminas, and the broader 'PSR-oriented' style of PHP development it exemplifies, rounds out your picture of how professional PHP is actually built in practice: sometimes as one cohesive framework, and sometimes as a hand-assembled set of interoperable parts, each chosen deliberately and swappable in principle because they all speak the same PHP-FIG standard interfaces.
What Laminas Is: Successor to Zend Framework
Laminas is the direct successor to Zend Framework, one of the oldest and most influential PHP frameworks, originally developed by Zend Technologies (the company behind the Zend Engine that powers PHP itself). In 2019, stewardship of Zend Framework was transferred to the Linux Foundation, and the project was rebranded as Laminas (the name itself a nod to 'lamina', a thin structural layer — fitting for a library-first project), continuing active development and maintenance under community governance rather than a single vendor.
This lineage matters because it explains Laminas's character: Zend Framework was, from its earliest versions, built as a loose collection of independently usable components rather than one tightly coupled monolith — you could use Zend\Mail or Zend\Validator in a project that used no other part of the framework at all. Laminas inherited and doubled down on that component-oriented design, and today ships dozens of laminas-* packages (validation, forms, DI, HTTP messaging, and more) each installable and usable on its own via Composer, independent of any specific application architecture.
Mezzio (formerly Zend Expressive) is Laminas's answer to a full request-handling application, built as a thin, PSR-15 middleware-based microframework on top of the underlying Laminas components — rather than being a monolithic 'framework' in the Symfony/Laravel sense, Mezzio is closer to a minimal routing and middleware pipeline that you assemble the rest of your stack around, using whichever Laminas (or third-party PSR-compliant) packages a given application actually needs.
• Laminas = the community-governed successor to Zend Framework, under Linux Foundation stewardship since 2019. • Zend Framework (and Laminas) were built component-first from the start — use one library without adopting a whole framework. • laminas-* packages (validator, mail, di, servicemanager, etc.) are independently installable via Composer. • Mezzio (formerly Zend Expressive) is Laminas's lightweight, PSR-15 middleware-based microframework for full applications.
Library-First Philosophy vs. Full-Stack Frameworks
A full-stack framework like Symfony or Laravel makes many decisions for you up front — a specific ORM, a specific templating engine, a specific way to structure controllers and routes — in exchange for a cohesive, well-documented, batteries-included experience where every piece is designed to work smoothly with every other piece. This is an enormous productivity win for the common case, and it's why most PHP applications, especially smaller and mid-sized ones, are built this way.
Laminas's library-first philosophy inverts that trade-off: rather than one project deciding your ORM, your templating, your DI container, and your HTTP abstraction all at once, you choose each concern independently — perhaps laminas-servicemanager for DI, Doctrine (borrowed from the Symfony ecosystem) for persistence, Plates or Twig for templating, and Mezzio for routing/middleware — and wire them together yourself. This costs more upfront assembly work and documentation-reading across multiple projects, but buys real flexibility: you're never locked into a framework's specific opinion about, say, how forms should work, because you can swap that one piece without touching anything else.
This trade-off tends to matter most for organizations with unusual constraints — a legacy codebase that already uses a particular library and can't cleanly adopt a whole new framework around it, a team that wants precise control over startup performance and memory footprint for a high-traffic API, or a project migrating incrementally off an old Zend Framework 2/3 application, where Laminas's continued compatibility and component-level upgrade path is a deliberate advantage over a full framework rewrite.
• Full-stack framework: one project's opinions cover ORM, templating, DI, routing — cohesive, fast to start, less flexible. • Library-first (Laminas): pick and wire together independent packages for each concern — more assembly work, more flexibility. • Best fit: legacy Zend Framework migrations, high-control/high-performance APIs, teams with strong existing library preferences. • The trade-off is upfront integration effort versus long-term freedom from any single framework's opinions.
PSR-15 Middleware and PSR-7 HTTP Messages
PSR-7 is the PHP-FIG standard describing HTTP messages — RequestInterface, ResponseInterface, ServerRequestInterface, StreamInterface, UriInterface — as immutable value objects: calling a method like withHeader() on a PSR-7 request returns a new instance with that header set, rather than mutating the original in place. This immutability is deliberate: it makes HTTP messages safe to pass through a chain of independent processing steps without one step's changes leaking unexpectedly into another's view of the same object.
PSR-15 builds directly on PSR-7 to standardize middleware: a RequestHandlerInterface has a single handle(ServerRequestInterface $request): ResponseInterface method, and a MiddlewareInterface has a single process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface method, where the middleware can inspect/modify the request, call $handler->handle($request) to continue the chain, and inspect/modify the resulting response before returning it. Because both interfaces are just two methods each, any PSR-15-compliant middleware — authentication, CORS handling, request logging, rate limiting — can be dropped into any PSR-15-compliant application, including a Mezzio app, without caring which library produced it.
This is the essential difference from a framework-specific middleware system: Symfony's event-based kernel middleware and Laravel's middleware classes work well within their own frameworks but aren't directly portable to each other; a PSR-15 middleware package, by contrast, works unmodified in Mezzio, Slim, or any other PSR-15-compliant dispatcher, because the interface itself — not a particular framework's base class — is the contract.
• PSR-7: immutable HTTP message value objects (Request/Response/ServerRequest/Stream/Uri interfaces). • withHeader() and similar methods return a new instance rather than mutating in place. • PSR-15: MiddlewareInterface::process($request, $handler) and RequestHandlerInterface::handle($request) standardize the middleware pipeline. • A PSR-15 middleware package is portable across any compliant framework (Mezzio, Slim, etc.), unlike framework-specific middleware.
Dependency Injection via laminas-servicemanager
laminas-servicemanager is Laminas's own dependency injection container, usable standalone in any PHP project regardless of whether the rest of the application uses any other Laminas component. Unlike Symfony's container, which leans heavily on autowiring by default, ServiceManager traditionally favors explicit factory classes: a factories configuration array maps a service name (often a class name) to a factory class or callable responsible for constructing it, giving very precise, debuggable control over exactly how each service is built, at the cost of writing a bit more explicit wiring code per service.
A typical ServiceManager factory is a small invokable class implementing __invoke(ContainerInterface $container): object, pulling whatever dependencies it needs directly out of the container by name and passing them into the constructor of the service being built. This explicitness is a deliberate design choice consistent with Laminas's overall philosophy: nothing is inferred by convention, every wiring decision is visible in configuration or a factory class, which some teams find easier to reason about and debug in a large, long-lived codebase than 'magic' autowiring, even though it takes more lines of code to set up initially.
Because ServiceManager implements the PSR-11 ContainerInterface standard, it can also be swapped for (or used alongside) any other PSR-11-compliant container — like PHP-DI — in a Mezzio application, which is itself another expression of the same composable, standards-first philosophy running through this whole ecosystem.
• laminas-servicemanager: a standalone DI container usable without any other Laminas component. • Favors explicit factory classes (implementing __invoke(ContainerInterface $c)) over automatic autowiring. • Every service's construction is visible in a factory or config array — more setup, more explicit debuggability. • Implements PSR-11 ContainerInterface, so it's swappable with other PSR-11 containers like PHP-DI.
The Broader PSR-Oriented, Decoupled Ecosystem
Laminas is one prominent example of a much broader pattern in modern PHP: because PHP-FIG's PSRs (PSR-7 for HTTP messages, PSR-11 for containers, PSR-15 for middleware, PSR-3 for logging, and others) standardize the interfaces between components, a developer can assemble an entire application from independent, interoperable packages chosen purely on their individual merits, without any single vendor's framework dictating the whole stack. This is sometimes described as a 'decoupled' or 'PSR-oriented' approach, as distinct from adopting one opinionated framework wholesale.
Concretely, this looks like combining packages such as league/route (a fast, standalone PSR-15-compliant router from The PHP League, unaffiliated with any full framework), nyholm/psr7 (a minimal, high-performance PSR-7 implementation many projects choose specifically because it's lightweight and dependency-free), and php-di/php-di (a popular, autowiring-capable PSR-11 container, often chosen precisely because it offers Symfony-style autowiring convenience without requiring the rest of Symfony) — none of which know about each other at the source-code level, yet compose cleanly because each one honors the same PHP-FIG interfaces.
The practical benefit of this style is substitutability: because your application code depends on Psr\Http\Message\ServerRequestInterface or Psr\Container\ContainerInterface rather than a specific vendor's concrete class, swapping nyholm/psr7 for another PSR-7 implementation, or php-di for laminas-servicemanager, is possible with minimal application-level changes — a flexibility that's harder to achieve inside a single tightly-integrated full-stack framework, though it comes at the real cost of needing to research, choose, and keep updated more individual packages yourself rather than following one framework's guided path.
• PSR-oriented/decoupled approach: assemble a stack from independent packages that share PHP-FIG standard interfaces, rather than one framework. • Example composable stack: league/route (routing) + nyholm/psr7 (HTTP messages) + php-di (DI container). • Depending on PSR interfaces (not concrete vendor classes) makes individual pieces substitutable later. • Trade-off: more freedom and substitutability, but more research/decision/maintenance overhead than one opinionated framework.
A PSR-15 Middleware Implementation
process() calls $handler->handle($request) to continue the pipeline, then modifies the resulting response before returning it. withHeader() returns a new response instance (PSR-7 immutability) rather than mutating $response in place.
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RequestTimingMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$start = microtime(true);
$response = $handler->handle($request);
$elapsedMs = (microtime(true) - $start) * 1000;
return $response->withHeader('X-Response-Time-Ms', (string) round($elapsedMs, 2));
}
}Code Examples
Unlike Symfony's autowiring, ServiceManager requires an explicit factory: this one pulls a PDO service and a config value out of the container by name and passes them to ProductCatalog's constructor, making every dependency's origin fully visible in code rather than inferred.
league/route handles routing, nyholm/psr7 provides the PSR-7 request/response objects, and neither package has any dependency on or knowledge of the other — they compose purely through shared PSR-7 interfaces, with no framework gluing them together.
<?php
namespace App\Factory;
use App\Service\ProductCatalog;
use Psr\Container\ContainerInterface;
class ProductCatalogFactory
{
public function __invoke(ContainerInterface $container): ProductCatalog
{
return new ProductCatalog(
$container->get(\PDO::class),
$container->get('config')['catalog']['cache_ttl'],
);
}
}
// Registered in config:
// 'factories' => [
// ProductCatalog::class => ProductCatalogFactory::class,
// ],<?php
require 'vendor/autoload.php';
use League\Route\Router;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
$router = new Router();
$router->map('GET', '/health', function ($request) {
$psr17 = new Psr17Factory();
$response = $psr17->createResponse(200);
$response->getBody()->write(json_encode(['status' => 'ok']));
return $response->withHeader('Content-Type', 'application/json');
});
$psr17 = new Psr17Factory();
$creator = new ServerRequestCreator($psr17, $psr17, $psr17, $psr17);
$request = $creator->fromGlobals();
$response = $router->dispatch($request);
// emit $response to the actual HTTP response...Migrating a Legacy Zend Framework 2 Application
A common real-world scenario for Laminas is a company running a long-lived Zend Framework 2 application: because Laminas provides drop-in compatible successor packages and clear migration tooling, the team can upgrade component by component over time — swapping zend-validator for laminas-validator, then zend-mvc for laminas-mvc — rather than facing an all-or-nothing rewrite onto a different framework entirely.
$response->withHeader('X-Foo', 'bar'); // then return $response unchanged
$response = $response->withHeader('X-Foo', 'bar'); return $response;
PSR-7 objects are immutable — withHeader() (and similar with*() methods) return a brand-new instance rather than modifying the original in place, so the return value must be captured or the change is silently discarded.
// Depending directly on a concrete PSR-7 implementation class everywhere function handle(\Nyholm\Psr7\ServerRequest $request) {... } function handle(\Psr\Http\Message\ServerRequestInterface $request) {... }
Type-hinting the concrete Nyholm class instead of the PSR-7 interface defeats the entire point of building on standard interfaces — it locks the code to one specific PSR-7 implementation and makes swapping it later far harder.
What is Laminas the direct successor to? A. CodeIgniter B. Zend Framework C. CakePHP D. Symfony
B. Zend Framework — Laminas is the continuation of Zend Framework, with stewardship transferred to the Linux Foundation in 2019 and the project rebranded under community governance.
What does PSR-7 require of HTTP message objects like requests and responses? A. They must be mutable for performance B. They must be immutable value objects, so methods like withHeader() return a new instance C. They must extend a specific Laminas base class D. They must be stored as arrays, not objects
B. They must be immutable value objects, so methods like withHeader() return a new instance — PSR-7 defines HTTP messages as immutable value objects; a call like withHeader() returns a new instance with the change applied rather than mutating the original, making messages safe to pass through a processing chain.
How does laminas-servicemanager's default approach to dependency injection differ from Symfony's? A. It cannot inject any dependencies at all B. It favors explicit factory classes over automatic autowiring by convention C. It only works inside a full Mezzio application D. It requires no configuration whatsoever
B. It favors explicit factory classes over automatic autowiring by convention — ServiceManager traditionally relies on explicit factory classes (or config arrays) to construct each service, making every dependency's construction visible in code, whereas Symfony leans heavily on automatic autowiring by convention.
Why can a package like league/route be combined with nyholm/psr7 even though neither package knows about the other? A. Because both are maintained by the same organization B. Because they both depend on shared PSR (PHP-FIG) standard interfaces rather than on each other directly C. Because PHP automatically merges unrelated packages D. They actually cannot be combined without a framework
B. Because they both depend on shared PSR (PHP-FIG) standard interfaces rather than on each other directly — Both packages are built around PHP-FIG's standard interfaces (like PSR-7's HTTP message interfaces), so they interoperate through that shared contract without either package needing direct knowledge of, or a dependency on, the other.
Explain, in your own words, why a PSR-15 middleware class written for a Mezzio application can be reused unmodified in a Slim or other PSR-15-compliant application, while a Laravel middleware class generally cannot be reused in Symfony without rewriting it.
A PSR-15 middleware implements the standard MiddlewareInterface (process($request, $handler)) defined by PHP-FIG, which any PSR-15-compliant dispatcher — Mezzio, Slim, or others — knows how to invoke, regardless of which library wrote the middleware. Laravel and Symfony middleware, by contrast, each depend on that specific framework's own base classes, method signatures, and kernel conventions rather than a shared external interface, so a middleware class written for one has no defined meaning inside the other's dispatch pipeline without being rewritten to match it.
Think about what interface each middleware class actually implements versus what base class or convention it depends on.