Laminas Service Manager: explicit factory or reflection-based wiring
Choose between hand-written factories and ReflectionBasedAbstractFactory for Laminas service wiring, with config examples and validation steps.
13 Apr 2026, 13:54 UTC

The decision and your constraints
You are adding a class to a Laminas application and must register it with the service container. Laminas Service Manager supports two construction paths: an explicit factory class you author, or ReflectionBasedAbstractFactory, which inspects the constructor at runtime and injects matching services automatically. The decision changes where errors surface: explicit factories fail during development and testing, while reflective wiring can fail the first time a service is requested in production.
Use this guide when editing config/autoload/dependencies.global.php or config/autoload/dependencies.local.php, depending on whether the registration applies to all environments or only the current one.
Useful takeaway: use reflection only for simple classes whose constructor parameters are all typed class or interface names the container already knows. Use an explicit factory whenever construction needs configuration values, untyped scalars, conditional logic, or objects that are not themselves services.
Supported options side by side
| Factor | Explicit factory | ReflectionBasedAbstractFactory |
|---|---|---|
| Boilerplate | One factory class per service | No factory logic; extend the abstract factory |
| Constructor parameters | Any type, including scalars and config | Typed services only; scalars must be configured |
| Error timing | Unit testable; fails at build time | Runtime, on first $container->get() |
| Config visibility | Explicit entry in dependencies.factories | Implicit; harder to audit in large configs |
| Build performance | No reflection during construction | Reflection cost unless the container is compiled |
Trade-offs that matter in production
Explicit factories make the dependency graph visible. A new developer can read the factory and see exactly which services and configuration values are required. They are also straightforward to unit test in isolation because you can pass a mock container and assert the returned object.
The downside is more files. If your application has dozens of simple data mappers or clients, writing a factory for each one adds maintenance overhead.
ReflectionBasedAbstractFactory removes that boilerplate. It works well for value objects, clients, or mappers whose constructors only accept other services. The risk is silent breakage: renaming a constructor parameter or removing a type hint does not produce an error until the container tries to build the service. It also cannot resolve untyped string or int parameters without extra configuration.
Do not register both an explicit factory and reflective auto-wiring for the same service unless you intend one to override the other.
Implementation: explicit factory
Use this pattern when a service needs a configuration value. The example assumes a ReportGenerator class that requires an HTTP client and an API key from configuration.
<?php
declare(strict_types=1);
namespace App\Factory;
use App\Service\ReportGenerator;
use Laminas\Http\Client;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerInterface;
class ReportGeneratorFactory implements FactoryInterface
{
public function __invoke(
ContainerInterface $container,
string $requestedName,
?array $options = null
): ReportGenerator {
$config = $container->get('config');
$apiKey = $config['reports']['api_key'] ?? null;
if (!$apiKey) {
throw new RuntimeException('Missing reports.api_key in configuration');
}
$httpClient = $container->get(Client::class);
return new ReportGenerator($httpClient, $apiKey);
}
}
Register it in config/autoload/dependencies.global.php:
<?php
declare(strict_types=1);
return [
'dependencies' => [
'factories' => [
ReportGenerator::class => ReportGeneratorFactory::class,
],
],
];
Implementation: reflection-based factory
Use this pattern only when every constructor parameter is a typed service the container can resolve. No constructor logic is required.
<?php
declare(strict_types=1);
namespace App\Factory;
use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;
class ReportGeneratorFactory extends ReflectionBasedAbstractFactory
{
// No methods required. The container resolves constructor parameters by reflection.
}
Registration is identical to the explicit factory example. The container attempts to reflect on ReportGenerator::__construct() when the service is first requested.
Validate before deployment
Run these checks from the project root. They require read access to composer.json and config/autoload/; no write permissions are needed for inspection.
- Confirm the dependency is installed:
grep -n "laminas/laminas-servicemanager" composer.json
Expected result: a require entry. If absent, the container features described here are not available.
- Inspect existing factory registrations:
grep -n "factories" config/autoload/dependencies.global.php
Expected result: existing factory entries. File names vary between Laminas MVC and Mezzio skeletons.
- Syntax-check the PHP files:
php -l src/Factory/ReportGeneratorFactory.php
Expected result: No syntax errors detected. A parse error means the file will not load.
- Bootstrap the container and request the service:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
$container = require __DIR__ . '/config/container.php';
$service = $container->get(ReportGenerator::class);
echo get_class($service) . "\n";
Expected check: the script prints the service class name and exits without a container exception. Paths are placeholders; adjust them to your skeleton. A missing entry throws a container exception at the get() call.
Run the validation in development first. Do not run arbitrary bootstrap scripts in production without reviewing them.
Limitations and rollback
- Reflection cannot auto-wire untyped scalar parameters such as API keys, strings, or integers.
- Constructor signature changes break reflective resolution at runtime, not when you edit the code.
- Compiled containers can reduce reflection overhead, but they do not turn missing type hints into configuration errors.
Because this change is limited to configuration and source files, rollback means restoring the previous factory entry in config/autoload/dependencies.global.php and redeploying. No database migration is involved.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.