0

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.

3 Answers3

2

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.

Community
  • 1
  • 1
dualmon
  • 1,225
  • 1
  • 8
  • 16
  • Great answer. Early I read that closure isn't good and may be cache errors. But I decided to implements ServiceLocatorAwareInterface. In future I'd like to use another services in my plugin so I passed the instance of ServiceLocator inside it. –  Oct 21 '15 at 12:50
  • Thanks. Really hope you reconsider injecting the locator. This is a major code smell. It inherently breaks encapsulation. – dualmon Oct 21 '15 at 18:50
1

I created it via ServiceLocatorAwareInterface and made it as invokable:

namespace Application\Controller\Plugin;

use Zend\Mvc\Controller\Plugin\AbstractPlugin;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class AppPlugin extends AbstractPlugin implements ServiceLocatorAwareInterface {

    /**
     * @var ServiceLocatorInterface
     */
    protected $serviceLocator;

    /**
     * Get system information
     * @return type
     */
    public function getAppInfo() {

        $config = $this->getServiceLocator()->get('Config');
        // to do something...
    }

    /**
     * Retrieve service manager instance
     *
     * @return ServiceLocator
     */
    public function getServiceLocator() {
        return $this->serviceLocator->getServiceLocator();
    }

    /**
     * Set service locator
     *
     * @param ServiceLocatorInterface $serviceLocator
     */
    public function setServiceLocator(ServiceLocatorInterface $serviceLocator) {
        $this->serviceLocator = $serviceLocator;
    }
}

Then in module.appplication.config:

'controller_plugins' => [
    'invokables' => [
        'AppPlugin'     => \Application\Controller\Plugin\AppPlugin::class
         ]
    ]

...and now I can use my plugin in controller like this:

$app = $this->AppPlugin()->getAppInfo();
0

You cannot instantiate the class directly in the controller you need to get it out of the controller plugin manager with the key 'Application\Plugin\AppPlugin'

$pluginManager = $this->getPluginManager();
$appPlugin = $pluginManager->get('Application\Plugin\AppPlugin');
$appPlugin->getAppInfo();

Alternatively you could change method to:

public function getAppInfo() 
{
    $serviceManager = $this->controller->getServiceLocator();
    $config = $serviceManager->get('Config');
}
Purple Hexagon
  • 3,538
  • 2
  • 24
  • 45
  • This is true, and he could take it to the next level and inject the plugin into the controller via a controller factory, which would give the controller an explicit dependency, rather than having the controller break encapsulation by accessing the locator directly. – dualmon Oct 21 '15 at 08:24