Decoupling PHP Logic with Laminas ServiceManager Factories
Learn how to use the Laminas ServiceManager and Factories to remove hard-coded dependencies from your PHP application, improving testability and decoupling.
17 Sept 2025, 20:04 UTC

The Problem: The New Keyword Trap
When you use the new keyword inside a class to instantiate a dependency, you create a hard-coded link between two components. If your OrderProcessor class manually creates a DatabaseConnection, you cannot easily swap that connection for a mock during testing or change the database driver without editing the business logic. This is the "hard dependency" problem.
The solution is to move the responsibility of object creation outside the class. In the Laminas ecosystem, the ServiceManager handles this by acting as a Dependency Injection (DI) container. Instead of a class asking for a specific instance, it asks for an interface, and the ServiceManager provides the concrete implementation via a Factory.
Using Factories for Clean Injection
A factory is a callable or a class that implements the FactoryInterface. Its sole job is to retrieve the necessary dependencies from the container and inject them into the target service's constructor. This keeps your business logic "pure" because the service doesn't know how its dependencies are created; it only knows it has them.
The ServiceManager Lifecycle
- Registration: You map a service name (usually the class name) to a factory in a configuration array.
- Resolution: When
get()is called, the ServiceManager checks if the instance already exists (singleton behavior by default). - Instantiation: If not present, the ServiceManager executes the factory, passing itself as an argument so the factory can pull other required services.
Worked Example: Injecting a Mailer into a UserRegistration Service
Assume we have a UserRegistrationService that requires a MailerInterface to send welcome emails. We want to avoid instantiating the Mailer inside the registration service.
1. The Service and Factory
// src/Service/UserRegistrationService.php
namespace App\Service;
class UserRegistrationService {
private $mailer;
public function __construct($mailer) {
$this->mailer = $mailer;
}
public function register($user) {
// Registration logic...
$this->mailer->sendWelcomeEmail($user);
}
}
// src/Factory/UserRegistrationServiceFactory.php
namespace App\Factory;
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\FactoryInterface;
use App\Service\UserRegistrationService;
use App\Service\MailerService;
class UserRegistrationServiceFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) {
// Pull the MailerService from the container to inject it
$mailer = $container->get(MailerService::class);
return new UserRegistrationService($mailer);
}
}
2. The Configuration
Run this configuration in your module.config.php or global service config:
return [
'service_manager' => [
'factories' => [
App\Service\UserRegistrationService::class => App\Factory\UserRegistrationServiceFactory::class,
App\Service\MailerService::class => Laminas\ServiceManager\Factory\InvokableFactory::class,
],
],
];
3. Verification
To verify the setup, initialize the ServiceManager and attempt to retrieve the service. If the factory is misconfigured or a dependency is missing, Laminas will throw a ServiceNotCreatedException.
// Run this in a bootstrap file or test case
$serviceManager = new \Laminas\ServiceManager\ServiceManager($config);
$registrationService = $serviceManager->get(App\Service\UserRegistrationService::class);
if ($registrationService instanceof App\Service\UserRegistrationService) {
echo "Service successfully instantiated with dependencies.";
}
Trade-offs and Limitations
While the ServiceManager provides immense flexibility, it introduces specific risks:
- The Service Locator Anti-Pattern: Avoid passing the
ServiceManageritself into your business classes. If a class calls$container->get()internally, it becomes a "Service Locator," hiding its dependencies and making unit tests harder because you must mock the entire container rather than just one dependency. - Circular Dependencies: If Service A requires Service B, and Service B requires Service A, the ServiceManager will enter an infinite loop or throw a fatal error. This is usually a sign that a third service should be extracted to hold the shared logic.
- Configuration Overhead: In very large applications, the
service_managerconfiguration array can grow massive, which may slightly increase memory usage during the initial bootstrap phase.
Actionable Closing
To move toward a decoupled architecture, start by auditing your classes for the new keyword. Replace those manual instantiations with constructor injection and create corresponding factories in the ServiceManager. This transition ensures your application remains testable and adaptable as your infrastructure evolves.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.