Architecting Dependency Injection with Laminas ServiceManager
Learn how to implement a scalable dependency injection architecture using Laminas ServiceManager, focusing on factory-based wiring, avoiding service locator pitfalls, and managing circular dependencies.
03 Jun 2026, 20:09 UTC

The Problem: Tight Coupling and Bootstrap Bloat
In large PHP applications, hard-coding class dependencies leads to "fragile" code where a change in one constructor requires updates across dozens of files. Furthermore, instantiating every possible dependency during the application bootstrap—even those not used in the current request—increases memory consumption and slows response times.
The Laminas ServiceManager solves this by acting as a Dependency Injection Container (DIC). The goal is to move the responsibility of object creation out of the business logic and into a centralized configuration layer.
The Minimal Design: Factory-Based Wiring
The most sustainable design in Laminas avoids using the ServiceManager as a "Service Locator" (passing the container itself into your classes). Instead, use Factories to inject only the specific dependencies a class requires.
A Factory is a callable or a class that implements ServiceManager\Factory\FactoryInterface. It receives the container as an argument and returns the fully configured service.
Implementation Example
Consider a UserReportService that requires a DatabaseAdapter and a Logger. Instead of the service fetching these from the container, the factory handles the wiring.
// src/Service/UserReportServiceFactory.php
namespace App\Service;
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\FactoryInterface;
class UserReportServiceFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
// Pull dependencies from the container
$db = $container->get(\App\Db\DatabaseAdapter::class);
$logger = $container->get(\App\Log\Logger::class);
// Inject them into the constructor
return new UserReportService($db, $logger);
}
}
Register this in your configuration array:
return [
'service_manager' => [
'factories' => [
\App\Service\UserReportService::class => \App\Service\UserReportServiceFactory::class,
],
],
];
Trust and Data Boundaries
The ServiceManager creates a boundary between Configuration (how objects are built) and Execution (how objects behave). To maintain this boundary:
- No Container Leakage: Business logic classes must never type-hint
ContainerInterface. If a class needs the container to resolve services dynamically, it is likely a Plugin Manager candidate rather than a standard service. - Interface Binding: Register services using interface names as keys. This allows you to swap a
FilesystemLoggerfor aCloudLoggerin the configuration without touching theUserReportServicecode.
Operational Checks and Verification
To verify the wiring is functioning as intended, perform these checks in a development environment:
| Check | Method | Expected Result |
|---|---|---|
| Singleton Behavior | Call $container->get('Service') twice and compare with ===. |
True (Same instance returned). |
| Missing Service | Request a non-registered string. | ServiceNotFoundException thrown. |
| Dependency Flow | Xdebug trace the Factory __invoke method. |
Factory is called only once per request. |
Failure Modes
Circular Dependencies
If ServiceA requires ServiceB, and ServiceB requires ServiceA, the ServiceManager will enter an infinite loop or trigger a fatal error during instantiation. To resolve this, introduce a third service to hold the shared state or use Lazy Services.
Lazy Services and Proxy Patterns
For heavy services (e.g., a mailer that connects to a remote API), use the laminas-servicemanager-proxy. This creates a "virtual proxy" that only instantiates the real object when a method is actually called, preventing bootstrap lag for requests that don't use the mailer.
Conditions for Design Evolution
The factory-per-service approach is ideal for most apps, but you should evolve the design if:
- Dynamic Strategy Selection: If you have 20 different "Payment Gateways" that share an interface, move them into a Plugin Manager. This provides a specialized sub-container that validates that every retrieved object implements the required interface.
- High-Frequency Instantiation: If you need new instances of a class every time (rather than a singleton), move the registration from
factoriestoinvokablesor use a custom factory that returns anew` instance instead of a cached one.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.