Refactoring is a way of life. If you write code, you refactor. Sometimes it’s for understand ability or to remove technical debt, others times it’s because your code sucks or a package you’re using changes. If you’re getting messages about the ServiceLocatorAwareInterface being deprecated, it’s like a sign from a power above (in this case Zend Framework) that you need to be thinking about refactoring your code. In the following blog, I’ll show you one way to go about removing your dependency on ServiceLocatorAwareInterface through the use of dependency injection with factories.
The ServiceLocatorAwareInterface is a pattern that handles the automatic injection of the ServiceManager into classes. This is arguably more of an anti-pattern because it exposes the entire service locator class as opposed to only the pieces that will be needed, making classes unpredictable, brittle, and very hard to unit test, since they can reach out to the service locator and retrieve anything. This throws the base principle of inversion of control out the window. A cleaner way to do this, is to explicitly inject only the services the class depends on: dependency injection.
If you’ve found this post, you’re probably seeing this warning crop up in your app, after doing a composer update. I’m a huge fan of PhpStorm, and will talk in specifics about how I use it to quickly and efficiently refactor a controller from direct use of the service locator to constructor injection of dependencies.
ServiceLocatorAwareInterface Deprecation Warning
Deprecated: ServiceLocatorAwareInterface is deprecated and will be removed in version 3.0, along with the ServiceLocatorAwareInitializer. Please update your class ApplicationControllerApplicationDetailsController to remove the implementation, and start injecting your dependencies via factory instead. in /var/www/vendor/zendframework/zend-mvc/src/Controller/ControllerManager.php on line 243
Deprecated: You are retrieving the service locator from within the class ApplicationControllerApplicationDetailsController. Please be aware that ServiceLocatorAwareInterface is deprecated and will be removed in version 3.0, along with the ServiceLocatorAwareInitializer. You will need to update your class to accept all dependencies at creation, either via constructor arguments or setters, and use a factory to perform the injections. in /var/www/vendor/zendframework/zend-mvc/src/Controller/AbstractController.php on line 258
Background / Reference
- MWOP Post On Deprecating ServiceLocatorAware:
https://mwop.net/blog/2016-04-26-on-locators.html - Zend Framework core team Discussion:
https://github.com/zendframework/zendframework/issues/5168 - ZF3 Removal Commit:
https://github.com/zendframework/zend-mvc/pull/36 - For more information, see the official ZF3 Service Manager Migration Guide:
https://zendframework.github.io/zend-servicemanager/migration/#migration-guide
Refactoring Example
The service locator can be used to retrieve all kinds of services and objects, but in this case, I’ll be extracting the application config and echoing out a custom parameter. You can see the custom parameter in the local config (see config/autoload/local.php below).
When it comes to refactoring, I try to keep the code working as much as I can while doing my refactoring so that if I get stuck, it’s a short trip back to working code. The basic idea is going to be to add a factory class that will be called to do the dependency injection. That way we can setup all the services we are going to need inside the factory and inject them into the controller when it’s being constructed.
Summary of Steps
The process entails the following steps (PhpStorm keyboard shortcuts are for osx):
- ApplicationDetailsController.php
- Add a constructor if needed. (cmd+n)
- Refactor local variable to a class variable. (cmd+opt+f )
- Setup the constructor to inject the class variable, with a type-hint if possible. (cmd+f6)
- ApplicationDetailsControllerFactory.php
- Create the factory and inject the dependency.
- module.config
- Update module.config.php to register the new factory with the service locator.
- ApplicationDetailsController.php
- Remove the reference to ServiceLocatorInterface from your controller.
Code Examples
<?php
return array(
'application_details' => [
'application_author' => 'jesse lavere',
],
);
Here’s the existing controller that implements ServiceLocatorAwareInterface and displays the deprecated error.
<?php
namespace ApplicationController;
use ZendMvcControllerAbstractActionController;
use ZendServiceManagerServiceLocatorAwareInterface;
use ZendViewModelViewModel;
class ApplicationDetailsController extends AbstractActionController implements ServiceLocatorAwareInterface
{
/**
* @return ViewModel
*/
public function indexAction()
{
if (method_exists($this->getServiceLocator(), 'getServiceLocator')) {
$applicationDetails = $this->getServiceLocator()->getServiceLocator()->get('config')['application_details'];
} else {
$applicationDetails = $this->getServiceLocator()->get('config')['application_details'];
}
return new ViewModel([
'author' => $applicationDetails['application_author'],
]);
}
}
There’s a difference in the way I think about how code works when I’m setting up an application compared to when I’m refactoring one. When I’m setting up an application, I think about the factory pattern as starting in the module.config.php file. Being first defined by the config that tells the controller that there is a factory that should be used in its construction.
<?php
return array(
'controllers' => array(
'factories' => array(
ApplicationControllerApplicationDetailsController::class =>
ApplicationControllerFactoryApplicationDetailsControllerFactory::class,
),
),
);
Then I think about the factory and what services will be needed to do the construction. Here in the controller factory, we get the application config and then inject it into the controller. If an additional service is needed in the controller, we just grab it with the service locator and add it as a parameter to the controller constructor and pass it in.
<?php
namespace ApplicationControllerFactory;
use ApplicationControllerApplicationDetailsController;
use ZendServiceManagerFactoryInterface;
use ZendServiceManagerServiceLocatorInterface;
class ApplicationDetailsControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $controllerLocator)
{
/**
* @var ServiceLocatorInterface $serviceLocator
*/
$serviceLocator = $controllerLocator->getServiceLocator();
return new ApplicationDetailsController(
$serviceLocator->get('config')['application_details']
);
}
}
Review the Class with the Controller
Finally, there’s the Controller that has services injected through the controller. This makes it very easy to look at the class and know all the services that are being consumed.
<?php
namespace ApplicationController;
use ZendMvcControllerAbstractActionController;
use ZendViewModelViewModel;
class ApplicationDetailsController extends AbstractActionController
{
/**
* @var array
*/
private $applicationDetails;
/**
* @param $applicationDetails
*/
public function __construct(array $applicationDetails)
{
$this->applicationDetails = $applicationDetails;
}
/**
* @return ViewModel
*/
public function indexAction()
{
return new ViewModel([
'author' => $this->applicationDetails['application_author'],
]);
}
}
To see the code in action, take a look at the github repo for this blog post:
https://github.com/lavere/RefactorZF2ServiceLocatorAwareInterface
Questions?
If you have a question or would like additional insights, contact our team today. We’re happy to share our thoughts with you.
Pingback: Zend Framework y su mensaje ΓÇ£DeprecatedΓǪ.ServiceLocatorAwareInterface.ΓÇ¥ – Mu la vaca
Good explanation, got it from first try. Thanks!