Decoupling Component Logic with Laminas ServiceManager Factories
Learn how to use the Laminas ServiceManager to decouple components using factories, invokables, and aliases to eliminate tight coupling and improve testability.
05 Dec 2025, 08:25 UTC

The Problem: Tight Coupling in Object Instantiation
When a class instantiates its own dependencies using the new keyword, it becomes tightly coupled to those specific implementations. This makes the code difficult to unit test because you cannot swap a real database connection or API client for a mock object. In large applications, this leads to “dependency hell,” where changing one low-level class requires updating every single class that instantiates it.
The solution is to move the responsibility of object creation to the Laminas ServiceManager. By using the ServiceManager, your business logic requests an interface or a service name, and the manager handles the instantiation and dependency injection (DI) behind the scenes.
Prerequisites
- PHP 8.0 or higher installed.
- Laminas ServiceManager installed via Composer:
composer require laminas/laminas-servicemanager. - A basic understanding of the Factory pattern.
Implementing a Factory-Based Dependency Flow
To decouple your components, you must move from direct instantiation to a factory-based configuration. A Factory is a class or callable that describes exactly how to create a service, including which other services must be injected into its constructor.
Step 1: Define the Service and its Dependency
Consider a UserReportService that requires a DatabaseAdapter. Instead of creating the adapter inside the service, define it in the constructor:
// src/Service/UserReportService.php
namespace App\\Service;
class UserReportService {
private $adapter;
public function __construct($adapter) {
$this->adapter = $adapter;
}
public function generate() {
// Use $this->adapter to fetch data
return "Report generated using " . get_class($this->adapter);
}
}
Step 2: Create the Factory
Factories must implement Laminas\\ServiceManager\\Factory\\FactoryInterface. The __invoke method provides access to the container, allowing you to pull the required dependencies.
// src/Factory/UserReportServiceFactory.php
namespace App\\Factory;
use Interop\\Container\\ContainerInterface;
use Laminas\\ServiceManager\\Factory\\FactoryInterface;
use App\\Service\\UserReportService;
class UserReportServiceFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) {
// Retrieve the dependency from the container
$adapter = $container->get('DbAdapter');
return new UserReportService($adapter);
}
}
Step 3: Configure the ServiceManager
Register your services in the configuration array. Use factories for complex objects and invokables for classes that require no constructor arguments.
use Laminas\\ServiceManager\\ServiceManager;
use App\\Factory\\UserReportServiceFactory;
use App\\Infrastructure\\DbAdapter;
$config = [
'factories' => [
// Complex service using a factory
App\\Service\\UserReportService::class => UserReportServiceFactory::class,
// Simple service with no dependencies
'DbAdapter' => Laminas\\ServiceManager\\Factory\\InvokableFactory::class,
],
'aliases' => [
'report_service' => App\\Service\\UserReportService::class,
],
];
$serviceManager = new ServiceManager($config);
Comparison: Invokables vs. Factories vs. Aliases
| Mechanism | Use Case | Dependency Handling |
|---|---|---|
| Invokable | Simple helper classes | None (Constructor must be empty) |
| Factory | Business logic/Services | Full control via ContainerInterface |
| Alias | Interface-to-Implementation | Points to another registered service |
Verification and Diagnostics
To ensure the ServiceManager is operating correctly, perform these three checks:
- Singleton Verification: Call
$serviceManager->get('report_service')twice. Compare the object hashes usingspl_object_hash(). They should be identical, confirming the service is a singleton and not being re-instantiated. - Lazy Loading Check: Place a
var_dump('Created')inside your factory's__invokemethod. Run the application; the message should only appear the first time the service is requested, not during the ServiceManager's initialization. - Dependency Swap: Change the
DbAdapterfactory to a mock class in your config. IfUserReportServicenow uses the mock without you changing its source code, the decoupling is successful.
Critical Limitations and Risks
- Service Locator Anti-Pattern: Never inject the
ServiceManageritself into your business classes. This hides dependencies and makes testing harder. Always inject only the specific objects the class needs. - Circular Dependencies: If Service A requires Service B, and Service B requires Service A, the ServiceManager will trigger a fatal error (stack overflow). Resolve this by introducing a third service or using a Setter injection pattern.
- Configuration Overhead: In production, large configuration arrays can slow down startup. Enable configuration caching in your application framework to avoid parsing the array on every request.
Rollback Procedure
If the ServiceManager implementation causes instability, revert the instantiation logic in the consuming class:
- Remove the
ServiceManager::get()call in the entry point. - Restore the
new Dependency()call inside the service constructor. - Delete the associated Factory class and remove the entry from the configuration array.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.