Stop Hard-Coding Dependencies: Mastering the Laminas ServiceManager
Stop using the 'new' keyword for dependencies. Learn how to use the Laminas (Zend) ServiceManager and Factories to decouple your business logic and enable true unit testing.
29 Apr 2026, 18:38 UTC

The 'New' Keyword Problem
When you use the new keyword inside a class to instantiate a dependency, you create a hard link between two components. If your OrderProcessor class manually creates a DatabaseConnection, you cannot easily swap that connection for a mock during testing, nor can you change the database driver without editing every class that uses it. This leads to rigid code that is difficult to maintain and nearly impossible to unit test.
The solution is to decouple instantiation (how an object is created) from execution (how the object is used). In the Laminas (formerly Zend Framework) ecosystem, the ServiceManager handles this by acting as a centralized registry for your application's dependencies.
Moving from Service Locator to Dependency Injection
A common mistake is passing the entire ServiceManager into your classes. This is known as the Service Locator pattern. While it seems convenient, it hides a class's true dependencies, making the code opaque. Instead, use the ServiceManager to perform Constructor Injection.
By defining a Factory, you tell the manager exactly what a service needs. The factory retrieves the dependencies from the container and injects them into the service's constructor. The resulting service remains completely unaware of the ServiceManager; it only knows it received the objects it needs to function.
Implementing a Custom Factory
To implement this, you need a service class and a corresponding factory that implements the FactoryInterface. In this example, we create a NewsletterService that requires a MailerInterface.
The Service Class
namespace App\Service;
class NewsletterService {
private $mailer;
public function __construct($mailer) {
$this->mailer = $mailer;
}
public function sendUpdate($email, $content) {
return $this->mailer->send($email, $content);
}
}The Factory
Run this logic within your application's service layer. The factory has access to the container, allowing it to pull the required mailer service before instantiating the newsletter service.
namespace App\Factory;
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\FactoryInterface;
use App\Service\NewsletterService;
class NewsletterServiceFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) {
// Retrieve the dependency from the container
$mailer = $container->get(\App\Mailer\SmtpMailer::class);
// Inject the dependency into the service
return new NewsletterService($mailer);
}
}The Configuration
Register the factory in your module.config.php or global configuration file. This maps the service name to the factory responsible for its creation.
return [
'service_manager' => [
'factories' => [
\App\Service\NewsletterService::class => \App\Factory\NewsletterServiceFactory::class,
],
],
];Advanced Orchestration: Delegators and Lazy Services
For complex applications, simple factories aren't always enough. Two powerful features help manage overhead and flexibility:
- Delegators: These allow you to wrap a service in a decorator. For example, if you want to add logging to a service without changing the original factory or the service class itself, a delegator can intercept the creation process and return a wrapped version of the object.
- Lazy Services: Some objects are "heavy" to instantiate (e.g., a remote API client that performs a handshake). Lazy services create a proxy object that only triggers the actual instantiation when a method is first called, reducing the bootstrap time of your request.
Trade-offs and Limitations
While the ServiceManager provides immense flexibility, it introduces a layer of abstraction that can make the code harder to navigate for newcomers. You can no longer "Cmd+Click" on a class instantiation to see where it's created; you must instead look at the configuration and the factory.
Additionally, deep dependency chains (Service A needs B, which needs C, which needs D) can slightly increase the initial memory footprint during the bootstrap phase. To verify your implementation is working as a singleton (the default behavior), you can retrieve the service twice and compare the object hashes:
$service1 = $container->get(\App\Service\NewsletterService::class);
$service2 = $container->get(\App\Service\NewsletterService::class);
var_dump(spl_object_hash($service1) === spl_object_hash($service2)); // Expected: trueActionable Closing
To clean up your current project, identify classes using the new keyword for external services. Create a FactoryInterface implementation for those classes, move the instantiation logic there, and register them in your service_manager config. This transition will immediately make your business logic more portable and your unit tests significantly easier to write.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.