Choosing Controller Dependency Injection Strategies in Zend Framework 2/3
Learn how to decide between invokable controllers, factory classes, closures and abstract factories for dependency injection in Zend Framework 2/3, with a concrete factory example and validation steps.
27 May 2026, 19:44 UTC

Decision: How to inject dependencies into ZF2/3 controllers
When building a controller that needs a service such as a database adapter or logger, you must decide how the ServiceManager will provide those dependencies without letting the controller reach into the container directly.
Constraints
- Using Zend Framework 2 or 3 with PSR‑4 autoloading via Composer.
- Controllers should remain unit‑testable and avoid the service‑locator anti‑pattern.
- Configuration must stay in
module.config.phpor a dedicatedservices.phpfile.
Supported options
| Option | How it works | When to use |
|---|---|---|
| Invokable controller | Controller class listed under invokables; ServiceManager creates it with new and no arguments. | Controller has no dependencies or only uses setters injected later. |
| Factory class | Implements Zend\ServiceManager\Factory\FactoryInterface (or the older FactoryInterface) and returns a fully configured controller instance. | Most common; keeps construction logic out of the controller. |
| Closure factory | Anonymous function registered under factories that receives $container and returns the controller. | Simple one‑off factories; avoid creating extra classes. |
| Abstract factory | Implements Zend\ServiceManager\Factory\AbstractFactoryInterface; can build many controllers that follow a naming convention. | Large numbers of similar controllers (e.g., REST controllers) where a pattern exists. |
Trade‑offs
- Invokable – zero configuration beyond the class name, but you must push dependencies via setters or rely on lazy properties, which can hide required dependencies and make testing harder.
- Factory class – explicit, testable, and keeps the controller constructor clean; adds a small file and requires the factory to be registered.
- Closure factory – reduces boilerplate for simple cases, but the closure lives in configuration and can become hard to read if logic grows.
- Abstract factory – eliminates repetitive factory registrations, yet introduces indirection and can make it harder to locate the creation logic for a specific controller.
Concrete implementation: factory class for a controller
Assume a controller Blog\Controller\PostController that needs a Blog\Service\PostService. The steps below show how to wire it with a factory class.
1. Define the controller
<?php
namespace Blog\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Blog\Service\PostService;
class PostController extends AbstractActionController
{
private $postService;
public function __construct(PostService $postService)
{
$this->postService = $postService;
}
public function indexAction()
{
$posts = $this->postService->fetchAll();
return new ViewModel(['posts' => $posts]);
}
}
2. Create the factory
<?php
namespace Blog\Controller\Factory;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Blog\Controller\PostController;
use Blog\Service\PostService;
class PostControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$postService = $container->get(PostService::class);
return new PostController($postService);
}
}
3. Register the factory in module configuration
<?php
return [
'controllers' => [
'factories' => [
Controller\PostController::class => Factory\PostControllerFactory::class,
],
],
'service_manager' => [
'factories' => [
Service\PostService::class => function($container) {
// example: inject a database adapter
$adapter = $container->get('Zend\Db\Adapter\Adapter');
return new Service\PostService($adapter);
},
],
],
];
4. Ensure the module is loaded
Add the module name to config/autoload/modules.config.php:
return [
'modules' => [
// …
'Blog',
],
];
Validation steps
- Run
composer show zendframework/zend-loaderto confirm the Zend Framework version is installed. - Verify
config/autoload/modules.config.phpcontains'Blog'in themodulesarray. - Bootstrap the application manually and retrieve the controller from the ServiceManager:
<?php
use Zend\Mvc\Application;
use Zend\ServiceManager\ServiceManager;
// minimal bootstrap for testing
$app = Application::init(require 'config/application.config.php');
$sm = $app->getServiceManager();
$controller = $sm->get('Blog\Controller\PostController');
// If no exception is thrown, the factory injected PostService successfully.
var_dump($controller instanceof Blog\Controller\PostController); // bool(true)
Check that var_dump prints bool(true) without any thrown exceptions.
Limitations
- Zend Framework 2/3 require PHP 5.6+; running on PHP 8.x may need compatibility layers or migration to Laminas.
- Factory classes add a small amount of boilerplate; for very simple controllers a closure factory may be preferable.
- If you rely on setters instead of constructor injection, required dependencies can be omitted silently, leading to runtime errors.
To confirm the wiring works in your specific environment, repeat the validation steps above after any changes to module.config.php or factory implementations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.