Search code examples
phpzend-framework2factoryservice-locator

Zend 2: Get service locator in plugin


I want to get service locator in my custom plugin. I create it through a factory in Module.php:

public function getControllerPluginConfig() {
    return [
        'factories' => [
            'Application\Plugin\AppPlugin' => function($serviceManager) {

                $serviceLocator = $serviceManager->getServiceLocator();
                $appPlugin = new \Application\Plugin\AppPlugin();
                $appPlugin->setLocator($serviceLocator);
                return $appPlugin;
            }  
        ]
    ];
}

...and my plugin:

<?php

namespace Application\Plugin;

use Zend\Mvc\Controller\Plugin\AbstractPlugin;

class AppPlugin extends AbstractPlugin {

    protected $locator;

    public function getAppInfo() {

        $config = $this->locator->get('Config');
    }

   /**
    * Set Service Locator
    * @param \Zend\ServiceManager\ServiceManager $locator
    */
    public function setLocator($locator) {
        $this->locator = $locator;
    }
}

Then I calling getAppInfo() in my controller:

$appPlugin = new AppPlugin();
$appPlugin->getAppInfo();

...and I get the error:

Fatal error: Call to a member function get() on a non-object in /vagrant/app/module/Application/src/Application/Plugin/AppPlugin.php on line 13
Call Stack
#   Time    Memory  Function    Location
1   0.0027  232104  {main}( )   ../index.php:0
2   0.6238  3166904 Zend\Mvc\Application->run( )    ../index.php:21
3   1.4410  5659112 Zend\EventManager\EventManager->trigger( )  ../Application.php:314
4   1.4410  5659112 Zend\EventManager\EventManager->triggerListeners( ) ../EventManager.php:205
5   1.4411  5660872 call_user_func ( )  ../EventManager.php:444
6   1.4411  5661440 Zend\Mvc\DispatchListener->onDispatch( )    ../EventManager.php:444
7   1.4505  5704600 Zend\Mvc\Controller\AbstractController->dispatch( ) ../DispatchListener.php:93
8   1.4505  5705080 Zend\EventManager\EventManager->trigger( )  ../AbstractController.php:118
9   1.4505  5705080 Zend\EventManager\EventManager->triggerListeners( ) ../EventManager.php:205
10  1.4507  5716656 call_user_func ( )  ../EventManager.php:444
11  1.4507  5716784 Zend\Mvc\Controller\AbstractActionController->onDispatch( ) ../EventManager.php:444
12  1.4508  5717504 Application\Controller\IndexController->indexAction( )  ../AbstractActionController.php:82
13  1.4641  5727768 Application\Plugin\AppPlugin->getAppInfo( ) ../IndexController.php:21

But if I'm passing service locator from my controller it works fine:

$appPlugin = new AppPlugin();
$appPlugin->setLocator($this->getServiceLocator());
$appPlugin->getAppInfo(); 

I'll be glad if someone will explain me what I'm doing wrong. Thanks.


Solution

  • When at all possible, you should not try to access the ServiceLocator inside any class except a factory. The main reason for this is that if the ServiceLocator is injected into your class, you now have no idea what that class's dependencies are, because it now could potentially contain anything.

    With regard to dependency injection, you have two basic choices: constructor or setter injection. As a rule of thumb, always prefer constructor injection. Setter injection should only be used for optional dependencies, and it also makes your code more ambiguous, because the class is now mutable. If you use purely constructor injection, your dependencies are immutable, and you can always be certain they will be there.

    Also rather than using a closure, it's generally better to use a concrete factory class, because closures cannot be opcode cached, and also your config array cannot be cached if it contains closures.

    See https://stackoverflow.com/a/18866169/1312094 for a good description of how to set up a class to implement FactoryInterface

    That being said, let's stick with your closure factory example for the purpose of this discussion. Rather than injecting the ServiceManager, we inject the config array (which is still a bit too much of a god dependency; better to inject the specific key of the config that you need, or better yet, an instantiated class).

    public function getControllerPluginConfig() {
        return [
            'factories' => [
                \Application\Plugin\AppPlugin::class => function($serviceManager) {
                    $serviceLocator = $serviceManager->getServiceLocator();
                    $config = $serviceLocator->get('Config');
                    $appPlugin = new \Application\Plugin\AppPlugin($config);
                    return $appPlugin;
                }  
            ]
        ];
    }
    

    And here the plugin is modified to take the config in the constructor. I assume this plugin code is just a proof of concept, and that you'll actually do something useful with the plugin besides returning the config, but hopefully this does show the pattern for injecting your plugin dependencies.

    <?php
    
    namespace Application\Plugin;
    
    use Zend\Mvc\Controller\Plugin\AbstractPlugin;
    
    class AppPlugin extends AbstractPlugin {
    
        protected $config;
    
        public function __construct($config){
             $this->config = $config;
        }
    
        public function getAppInfo() {
            return $this->config;
        }
    
    }
    

    Also, a small thing, but assuming PHP 5.5+, consider using \Application\Plugin\AppPlugin::class like I did in the example above, instead of a string literal for the config key.