Get the Factory in this post as a part of my (free) Zend Framework 3 Auto-Wiring Package!
The recent march toward ZF3 has signed the blockade that keeps the service locator out of all controllers. If you've been developing a ZF2 app, you're probably fixing your composer to not install zend-mvc 2.7+. Pause that version freeze, patch below.
The deprecation of this pivotal auto-service is ratified by an academic mix of "it's an anti-pattern", code rigor and testability - all true. However rationalized, the real world impact can be a PITA. If you want to stay up-to-date, you've got to face the music. I did anyways, in looking at the bevy of controllers I had to deal with if I wanted to keep current.
Anecdotal, I hit IRC quickly to ask "how are you guys dealing with this?". All I got was "not upgrading yet".
If you've read this far, you're in my shoes -- keep reading!
Here then, is an abstract factory that you can modify to help you get around this zend-mvc change. All you have to do, is write constructors with the right class names - and this abstract factory will use reflection to inject your dependencies for you. What's more, some cheat code can sub parts in, for example: if you use "array $config" or other aliases as constructor parameters, it'll get the ZF config and so forth.
The abstract factory relies on the fact that you are using ::class to declare your service manager components. If you're not using ::class, you should! e.g.:
'service_manager' => [
'factories' => [
SuperService::class => SuperServiceFactory::class,
],
],
Here's the abstract factory (tailed to Controllers) that uses reflection to identify dependencies.
class LazyControllerFactory implements AbstractFactoryInterface
{
/**
* Determine if we can create a service with name
*
* @param ServiceLocatorInterface $serviceLocator
* @param $name
* @param $requestedName
*
* @return bool
*/
public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
{
list( $module, ) = explode( '\\', __NAMESPACE__, 2 );
return strstr( $requestedName, $module . '\Controller') !== false;
}
/**
* These aliases work to substitute class names with SM types that are buried in ZF
* @var array
*/
protected $aliases = [
'Zend\Form\FormElementManager' => 'FormElementManager',
'Zend\Validator\ValidatorPluginManager' => 'ValidatorManager',
'Zend\Mvc\I18n\Translator' => 'translator',
];
/**
* Create service with name
*
* @param ServiceLocatorInterface $serviceLocator
* @param $name
* @param $requestedName
*
* @return mixed
*/
public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
{
$class = new \ReflectionClass($requestedName);
$parentLocator = $serviceLocator->getServiceLocator();
if( $constructor = $class->getConstructor() )
{
if( $params = $constructor->getParameters() )
{
$parameter_instances = [];
foreach( $params as $p )
{
if( $p->getClass() ) {
$cn = $p->getClass()->getName();
if (array_key_exists($cn, $this->aliases)) {
$cn = $this->aliases[$cn];
}
try {
$parameter_instances[] = $parentLocator->get($cn);
}
catch (\Exception $x) {
echo __CLASS__
. " couldn't create an instance of $cn to satisfy the constructor for $requestedName.";
exit;
}
}
else{
if( $p->isArray() && $p->getName() == 'config' )
$parameter_instances[] = $parentLocator->get('config');
}
}
return $class->newInstanceArgs($parameter_instances);
}
}
return new $requestedName;
}
}
Setup
Here's a run down of what your setup would look like in parts:
Abstract Factory | module.config.php
'controllers' => [
'abstract_factories' => [
LazyControllerFactory::class,
]
],
Route Setup | module.config.php
Your routes have to use class names, or it won't work.
'home' => [
'type' => 'Literal',
'options' => [
'route' => '/',
'defaults' => [
'controller' => \Application\Controller\IndexController::class,
'action' => 'index',
],
],
],
Controller | fix dependencies
Your controller constructor is where the factory discovers dependencies using reflection. Note, in cases where you have uber complex setup, you should probably stick to a bona-fide factory. That's ok, ZF will match factories before it does abstract factories. The lazy factory can be your backup.
public function __construct( FormElementManager $formElementManager, ConfigurationMapper $configurationMapper, array $config )
{
$this->formElementManager = $formElementManager;
$this->configurationMapper = $configurationMapper;
$this->countryConfig = $config['lemonade']['default_country'];
}
After this, it's just a matter of removing any reference to $this->getServiceLocator() in your controller code. Remove the locator, add the dependency you were locating to the constructor; rinse and repeat.
Good luck with your migration! I always welcome improvements and feedback!
I kept this in post-migration. It lets me prototype just as fast as when I had the SL readily available and makes things a bit less nebulous when comparing tests to controllers/services, etc.