Choosing Service Creation Mechanisms in Laminas ServiceManager: Factories, Abstract Factories, Delegators, and Initializers
A decision guide for Laminas ServiceManager: when to use factories, abstract factories, delegator factories, or initializers. Includes a comparison table, trade-offs, a complete module.config.php example, and a verification script to confirm delegation order.
08 Aug 2025, 13:36 UTC

The Decision: How to Wire Services in Laminas ServiceManager
Laminas ServiceManager (formerly Zend\ServiceManager) offers four distinct ways to create and configure services: factories, abstract factories, delegator factories, and initializers. Each serves a different lifecycle role, and picking the wrong one leads to hidden wiring, performance surprises, or maintenance debt. This guide states the constraints, compares the options, explains trade-offs, and shows a concrete configuration you can validate.
Constraints That Shape the Choice
- Explicitness vs. boilerplate – Factories require one class per service; abstract factories reduce repetition for families of similar services.
- Cross-cutting concerns – Logging, transactions, caching, or lazy-loading should wrap services without modifying their factories.
- Performance – Factories and delegators resolve in O(1) with cached config; abstract factories iterate until a match; initializers run on every service creation.
- Traceability – Explicit factories appear in configuration; abstract factories can mask typos by silently handling unknown service names.
- Legacy vs. modern – Initializers are deprecated in Laminas 3.4+ in favor of delegators; avoid them for new code.
Quick Comparison
| Mechanism | Interface | Primary Use | Resolution Cost | Scope | Deprecation Status |
|---|---|---|---|---|---|
| Factory | FactoryInterface | One class per service, full control over instantiation | O(1) cached | Per-service | Active |
| Abstract Factory | AbstractFactoryInterface | Fallback for families of services sharing a pattern (e.g., repositories) | O(n) abstract factories | Global fallback | Active |
| Delegator Factory | DelegatorFactoryInterface | Wrap/decorate existing services for cross-cutting concerns | O(1) per delegator | Per-service, stacked | Active |
| Initializer | InitializerInterface or callable | Post-creation injection (e.g., EventManagerAware) | Runs on every service | Global | Deprecated since 3.4 |
Trade-offs in Detail
Factories: Explicit and Testable
Implement FactoryInterface with a __invoke(ContainerInterface $container, $requestedName, array $options = null) method. Each service gets its own factory class, making dependencies visible and unit-testable. Use for any service with non-trivial construction or distinct dependencies.
Abstract Factories: Convenience with Hidden Wiring
Implement canCreateService(ContainerInterface $container, $requestedName) and createService(ContainerInterface $container, $requestedName). They act as a catch-all: when no explicit factory exists, ServiceManager iterates registered abstract factories until one returns true. This reduces boilerplate for families like Doctrine repositories, but a typo in a service name silently falls through instead of throwing. Enable strict mode or audit with $container->getRegisteredServices().
Delegator Factories: Decoration Without Modification
Implement __invoke(ContainerInterface $container, $name, callable $callback, array $options = null). The $callback produces the original service instance. Delegators stack in registration order (first registered wraps outermost). Ideal for logging, caching, transactions, or lazy-loading. Critical: call $callback() exactly once and return its result; multiple invocations create multiple instances.
Initializers: Global Side-Effects (Avoid)
Initializers run after every service creation, including framework internals. They receive the instance and container. Use only for mandatory interface injection in legacy ZF2 code. In Laminas 3.4+, prefer delegators; they are scoped, ordered, and don't affect unrelated services.
Concrete Implementation: A Module Demonstrating All Four
The following module.config.php registers a factory, an abstract factory, two delegators, and an initializer (for legacy demonstration). Place it in a module's config/ directory.
<?php
// module/Application/config/module.config.php
return [
'service_manager' => [
// Explicit factory for a domain service
'factories' => [
App\Service\UserService::class => App\Factory\UserServiceFactory::class,
],
// Abstract factory for repository family
'abstract_factories' => [
App\Factory\DoctrineRepositoryAbstractFactory::class,
],
// Delegators stack: TransactionDelegator wraps outermost, then LoggingDelegator
'delegators' => [
App\Service\UserService::class => [
App\Delegator\TransactionDelegator::class,
App\Delegator\LoggingDelegator::class,
],
// Apply logging to all repositories created by abstract factory
'*' => [
App\Delegator\LoggingDelegator::class,
],
],
// Initializer (legacy only) - runs for every service
'initializers' => [
App\Initializer\LegacyAwareInitializer::class,
],
// Aliases for convenience
'aliases' => [
'user-service' => App\Service\UserService::class,
],
],
];
Factory Example
<?php
// module/Application/src/Factory/UserServiceFactory.php
namespace App\Factory;
use App\Service\UserService;
use App\Repository\UserRepository;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerInterface;
class UserServiceFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): UserService
{
$repo = $container->get(UserRepository::class);
return new UserService($repo);
}
}
Abstract Factory Example
<?php
// module/Application/src/Factory/DoctrineRepositoryAbstractFactory.php
namespace App\Factory;
use App\Repository\AbstractRepository;
use Doctrine\ORM\EntityManagerInterface;
use Laminas\ServiceManager\AbstractFactory\AbstractFactoryInterface;
use Psr\Container\ContainerInterface;
class DoctrineRepositoryAbstractFactory implements AbstractFactoryInterface
{
public function canCreateService(ContainerInterface $container, $requestedName): bool
{
return is_a($requestedName, AbstractRepository::class, true);
}
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): AbstractRepository
{
$em = $container->get(EntityManagerInterface::class);
return new $requestedName($em);
}
}
Delegator Examples
<?php
// module/Application/src/Delegator/TransactionDelegator.php
namespace App\Delegator;
use App\Service\TransactionAwareInterface;
use Laminas\ServiceManager\DelegatorFactoryInterface;
use Psr\Container\ContainerInterface;
class TransactionDelegator implements DelegatorFactoryInterface
{
public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null)
{
$service = $callback(); // call exactly once
if ($service instanceof TransactionAwareInterface) {
$service->setTransactionManager($container->get('transaction-manager'));
}
return $service;
}
}
<?php
// module/Application/src/Delegator/LoggingDelegator.php
namespace App\Delegator;
use Psr\Log\LoggerInterface;
use Laminas\ServiceManager\DelegatorFactoryInterface;
use Psr\Container\ContainerInterface;
class LoggingDelegator implements DelegatorFactoryInterface
{
public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null)
{
$service = $callback();
$logger = $container->get(LoggerInterface::class);
// Wrap with a proxy that logs method calls (simplified)
return new class($service, $logger) implements \ProxyManager\Proxy\LazyLoadingInterface {
private $wrapped;
private $logger;
public function __construct($wrapped, $logger) {
$this->wrapped = $wrapped;
$this->logger = $logger;
}
public function __call($method, $args) {
$this->logger->info("Calling {$method} on " . get_class($this->wrapped));
return $this->wrapped->$method(...$args);
}
};
}
}
Initializer Example (Legacy)
<?php
// module/Application/src/Initializer/LegacyAwareInitializer.php
namespace App\Initializer;
use Laminas\EventManager\EventManagerAwareInterface;
use Laminas\ServiceManager\Initializer\InitializerInterface;
use Psr\Container\ContainerInterface;
class LegacyAwareInitializer implements InitializerInterface
{
public function __invoke($instance, ContainerInterface $container): void
{
if ($instance instanceof EventManagerAwareInterface) {
$instance->setEventManager($container->get('EventManager'));
}
}
}
Validation: Verify the Delegation Chain
Create a test script to confirm creation order and wrapper stacking. Run it from the project root after installing dependencies.
# 1. Create a minimal Laminas MVC skeleton (if not existing)
composer create-project laminas/laminas-mvc-skeleton test-app
cd test-app
# 2. Add the module configuration and classes above (or copy your module)
# 3. Write a verification script:
cat > verify.php <'EOF'
<?php
require 'vendor/autoload.php';
use Laminas\Mvc\Application;
use Laminas\ServiceManager\ServiceManager;
$app = Application::init(require 'config/application.config.php');
$container = $app->getServiceManager();
// Track creation order
echo "=== Retrieving UserService (explicit factory + delegators) ===\n";
$userService = $container->get('user-service');
echo "UserService class: " . get_class($userService) . "\n";
// Retrieve a repository (abstract factory + delegator)
echo "\n=== Retrieving UserRepository (abstract factory + logging delegator) ===\n";
$repo = $container->get('App\\Repository\\UserRepository');
echo "Repository class: " . get_class($repo) . "\n";
// Check registered services
echo "\n=== Registered services (sample) ===\n";
$registered = $container->getRegisteredServices();
echo "Count: " . count($registered) . "\n";
foreach (array_slice($registered, 0, 10) as $svc) {
echo " - $svc\n";
}
EOF
# 4. Run verification
php verify.php
Expected checks:
- UserService is wrapped by TransactionDelegator (outermost) then LoggingDelegator (innermost).
- UserRepository is created by the abstract factory and wrapped by LoggingDelegator.
- Initializer runs for both services (and every other service) – you'll see EventManager injection if logged.
- No CircularDependencyException; if one appears, extract shared state to a separate service or use lazy services (ProxyManager).
Limitations and Practical Checks
- Abstract factory masking: A typo like
UserServciemay be handled by an abstract factory instead of throwing. Run$container->getRegisteredServices()and compare with expected service list. - Delegator callback misuse: Calling
$callback()multiple times creates multiple instances. Static analysis (Psalm/PHPStan) can flag unused return values. - Initializer global impact: Type-check with
instanceofbefore injecting; otherwise framework internals may break. - Case sensitivity: Service names are case-sensitive strings. Use fully-qualified class names as service names to avoid collisions.
- PSR-11 container in Mezzio/Expressive: Delegators and initializers must be registered in the underlying ServiceManager config, not the PSR-11 wrapper.
To audit your configuration, run php verify.php and confirm the output matches the expected delegation chain. For production, enable config_cache_enabled in config/autoload/global.php to cache merged service configuration.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.