Reputation: 455
Since version 2.7.0 of zend-mvc the ServiceLocatorAwareInterface
is depricated, so are $this->serviceLocator->get()
calls inside controllers.
Thats why some days ago I did a huge refactoring of all my modules to inject the needed services/objects through constructors using factories for mostly everything.
Sure, I understand why this is the better/cleaner way to do things, because dependendies are much more visible now. But on the other side:
This leads to a heavy overhead and much more never-used class instances, doesn't it?
Let's look to an example:
Because all my controllers having dependencies, I've created factories for all of them.
CustomerControllerFactory.php
namespace Admin\Factory\Controller;
class CustomerControllerFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $controllerManager) {
$serviceLocator = $controllerManager->getServiceLocator();
$customerService = $serviceLocator->get('Admin\Service\CustomerService');
$restSyncService = $serviceLocator->get('Admin\Service\SyncRestClientService');
return new \Admin\Controller\CustomerController($customerService, $restSyncService);
}
}
CustomerController.php
namespace Admin\Controller;
class CustomerController extends AbstractRestfulController {
public function __construct($customerService, $restSyncService) {
$this->customerService = $customerService;
$this->restSyncService = $restSyncService;
}
}
module.config.php
'controllers' => [
'factories' => [
'Admin\Controller\CustomerController' => 'Admin\Factory\Controller\CustomerControllerFactory',
]
],
'service_manager' => [
'factories' => [
'Admin\Service\SyncRestClientService' => 'Admin\Factory\SyncRestClientServiceFactory',
]
]
SyncRestClientServiceFactory.php
namespace Admin\Factory;
class SyncRestClientServiceFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $serviceLocator) {
$entityManager = $serviceLocator->get('doctrine.entitymanager.orm_default');
$x1 = $serviceLocator->get(...);
$x2 = $serviceLocator->get(...);
$x3 = $serviceLocator->get(...);
// ...
return new \Admin\Service\SyncRestClientService($entityManager, $x1, $x2, $x3, ...);
}
}
The SyncRestService is a complex service class which queries some internal server of our system. It has a lot of dependencies, and is always created if a request comes to the CustomerController. But this sync-service is only used inside the syncAction()
of the CustomerController! Before I was using simply $this->serviceLocator->get('Admin\Service\SyncRestClientService')
inside the syncAction()
so only then it was instantiated.
In general it looks like a lot of instances are created through factories at every request, but the most dependencies are not used. Is this an issue because of my design or it is a normal side-effect behaviour of "doing dependency injection through constructors"?
Upvotes: 4
Views: 583
Reputation: 44383
If you only use your SyncRestClientService
inside a controller you should consider changing it from a service to a controller plugin (or make a controller plugin where you inject your SyncRestClientService
).
Like that you can still get it inside your controller syncAction
method very similar to like you did before. This is exactly the purpose of the ZF2 controller plugins.
First you need to create your controller plugin class (extending Zend\Mvc\Controller\Plugin\AbstractPlugin
):
<?php
namespace Application\Controller\Plugin;
use Zend\Mvc\Controller\Plugin\AbstractPlugin;
class SyncPlugin extends AbstractPlugin{
protected $syncRestClientService;
public function __constuct(SyncRestClientService $syncRestClientService){
$this->syncRestClientService = $syncRestClientService
}
public function sync(){
// do your syncing using the service that was injected
}
}
Then a factory to inject your service in the class:
<?php
namespace Application\Controller\Plugin\Factory;
use Application\Controller\Plugin\SyncPlugin;
class SyncPluginFactory implements FactoryInterface
{
/**
* @param ServiceLocatorInterface $serviceController
* @return SyncPlugin
*/
public function createService(ServiceLocatorInterface $serviceController)
{
$serviceManager = $serviceController->getServiceLocator();
$syncRestClientService = $serviceManager>get('Admin\Service\SyncRestClientService');
return new SyncPlugin($syncRestClientService);
}
}
Then you need to register your plugin in your module.config.php
:
<?php
return array(
//...
'controller_plugins' => array(
'factories' => array(
'SyncPlugin' => 'Application\Controller\Plugin\Factory\SyncPluginFactory',
)
),
// ...
);
Now you can use it inside your controller action like this:
protected function syncAction(){
$plugin = $this->plugin('SyncPlugin');
//now you can call your sync logic using the plugin
$plugin->sync();
}
Read more on controller plugins here in the documentation
Upvotes: 1
Reputation: 1485
Personally I get the action name in the controller factory to inject services on a per action basis.
Have a look at my sites controller.
namespace Admin\Controller\Service;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Admin\Controller\SitesController;
use Admin\Model\Sites as Models;
class SitesControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$actionName = $serviceLocator->getServiceLocator()->get('Application')->getMvcEvent()->getRouteMatch()->getParam('action');
$controller = new SitesController();
switch ($actionName) {
case 'list':
$controller->setModel($serviceLocator->getServiceLocator()->get(Models\ListSitesModel::class));
break;
case 'view':
$controller->setModel($serviceLocator->getServiceLocator()->get(Models\ViewSiteModel::class));
break;
case 'add':
$controller->setModel($serviceLocator->getServiceLocator()->get(Models\AddSiteModel::class));
break;
case 'edit':
$controller->setModel($serviceLocator->getServiceLocator()->get(Models\EditSiteModel::class));
break;
}
return $controller;
}
}
As you can see I use $serviceLocator->getServiceLocator()->get('Application')->getMvcEvent()->getRouteMatch()->getParam('action');
to get the action name and use a switch statement to inject the dependencies when required.
I don't know if this is the best solution but it works for me.
Hope this helps.
Upvotes: 0
Reputation: 3534
Maybe you only need one dependency to be injected into the controller constructor (the ServiceManager instance). I don't see any cops around...
namespace Admin\Factory\Controller;
class CustomerControllerFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $controllerManager)
{
$serviceLocator = $controllerManager->getServiceLocator();
return new \Admin\Controller\CustomerController($serviceLocator);
}
}
Upvotes: 0
Reputation: 9008
In my opinion it is a normal effect of dependency injection through constructors.
I think you have now two options (not mutually exclusive) to improve how your application works:
Split your controllers, so that the dependencies are instanciated only when needed. This would certainly give rise to more classes, more factories, and so on, but your code would attain more to the single responsability principle
You could use Lazy Services, so that, even if some services are dependencies of the whole controller, they will be actually instanciated only the first time they are called (so never for the actions where they are not called!)
Upvotes: 8