jurian sluiman

{
Wissel naar

Using Zend Framework service managers in your application

Zend Framework 2 uses a ServiceManager component (in short, SM) to easily apply inversion of control. I notice there are good resources about the background of service managers (I recommend this blog post from Evan or this post from Reese Wilson) but many people still have problems to tune the SM to their needs. In this post I will try to explain the reason why the framework uses multiple service managers and how you can use these. I address the following topics:

  1. What are the different service managers?
  2. For what reason are different managers used?
  3. How does the service locator relate to the service manager?
  4. How can you define services for all those service managers?
  5. How can you retrieve services from one manager inside a second one?

Service managers are used in Zend Framework 2 at a variety of places, but these four are the most important ones:

  1. General application services ("root service manager" or "main service manager")
  2. Controllers
  3. Controller plugins
  4. View helpers

Every group has its own service manager with the benefit you can have one service key for different services. Perhaps you know there is a "url" view helper, but also a "url" controller plugin. This would be really hard to achieve when you have one service manager where the "url" key needs to be put into context. With multiple managers, you can easily keep track of them both.

There is also the aspect of security. You might have a route where you have a parameter for the controller. By typing in a special url, the service manager tries to instantiate that service for you. If you don't care about security too much, you might accidentally instantiate all kinds of objects by requesting special urls.

Difference between the manager and the locator

Many people ask questions about the difference between the service locator and the service manager. The service locator (or SL) is an interface which is very slim:

namespace Zend\ServiceManager;

interface ServiceLocatorInterface
{
    public function get($name);
    public function has($name);
}

The service manager is a service locator implementation. By default the Zend Framework 2 implementation of the SL is the SM. Throughout the framework you see sometimes getServiceLocator() methods and sometimes getServiceManager() methods. For getServiceLocator(), you get the SL returned and for getServiceManager() you explicitly ask for the SM implementation.

It is not a big difference at this moment, since both methods will return the same object. However you can choose to have a different SL implementation. You keep yourself to the SL contract, but several zf2 components still need the specific SM implementation.

Configuration of the service manager

The service managers can be configured in two ways: the module class can return the SM config and the module configuration file (config/module.config.php in most cases) can return SM config. Both result in the exact same service config so it is only a matter of taste where you would like to put the config.

You can add services in either of these ways:

/**
 * With the module class
 */
namespace MyModule;

class Module
{
  public function getServiceConfig()
  {
    return array(
      'invokables' => array(
        'my-foo' => 'MyModule\Foo\Bar',
      ),
    );
  }
}
/**
 * With the module config
 */
return array(
  'service_manager' => array(
    'invokables' => array(
      'my-foo' => 'MyModule\Foo\Bar'
    ),
  ),
);

As you see, for both methods the content of the array is the same. This is true for all four types of service managers. With the module class method, you can duck type the method and the config will be loaded. You can also play by the contract and add an interface where you are more strict in the declaration of this method. With an interface applied, your module class could look like this:

namespace MyModule;

use Zend\ModuleManager\Feature\ServiceProviderInterface;

class Module implements ServiceProviderInterface
{
  public function getServiceConfig()
  {
    return array(
      'invokables' => array(
        'my-foo' => 'MyModule\Foo\Bar',
      ),
    );
  }
}

For all four service managers, you can add a key to your module config or add a method to your module class. For the later, you can choose to duck type with the method or add a Zend\ModuleManager\Feature\* interface. The table below shows the link between all of them. The "manager" states what it manages, the class name is provided, the key in the module configuration is provided and the class name & interface for the module class is provided. For the controller, controller plugin and view helper managers, the service name is also mentioned as those service instances are registered as a service itself in the main application service manager (more on that later).

Manager Application services
Manager class Zend\ServiceManager\ServiceManager
Config key service_manager
Module method getServiceConfig()
Module interface ServiceProviderInterface
Manager Controllers
Manager class Zend\Mvc\Controller\ControllerManager
Config key controllers
Module method getControllerConfig()
Module interface ControllerProviderInterface
Service name ControllerLoader
Manager Controller plugins
Manager class Zend\Mvc\Controller\PluginManager
Config key controller_plugins
Module method getControllerPluginConfig()
Module interface ControllerPluginProviderInterface
Service name ControllerPluginManager
Manager View helpers
Manager class Zend\View\HelperPluginManager
Config key view_helpers
Module method getViewHelperConfig()
Module interface ViewHelperProviderInterface
Service name ViewHelperManager

Be careful

There is one catch you need to be aware of. As Evan explains, there are two options for a factory. You can have a closure or a string pointing to a class. This class must implement the Zend\ServiceManager\FactoryInterface or it must have an __invoke method. The factories can be places inside the module config and in the module class.

If you have a closure and place this inside the module.config.php, then you will get a problem. All the module configurations can be cached as a big merged config. The problem with php is that closures cannot be serialized. So you either have to use factory classes in your module.config.php or you must use the getServiceConfig() method and alike to use closures.

The root manager versus the others

The name root (or also "main") is often used in discussions on IRC for example, but it not really related to any naming of the Zend Framework 2 code base. The name probably comes from the idea the Zend\ServiceManager\ServiceManager holds all main services and the other managers are more specific for one type of services. The name "root" suggests there is a relation between some managers. And guess? Yes, there is a link!

Imagine you have a controller where you want to inject a cache storage instance into. The controller has it's factory inside the controller service manager. The cache is a service in the root service manager. How do you get the cache service inside your controller factory? That's where the link comes from. The controller, controller plugin and view helper service managers are an implementation of Zend\ServiceManager\AbstractPluginManager. This class has a method getServiceLocator() which returns the root service locator. This makes it possible to travel around between the different managers:

use MyModule\Controller;

return array(
  'controllers' => array(
    'factories' => array(
      'MyModule\Controller\Foo' => function($sm) {
        $controller = new Controller\FooController;

        $cache = $sm->getServiceLocator()->get('my-cache');
        $controller->setCache($cache);

        return $controller;
      },
    ),
  ),
);

Here the cache service is located in the root service locator and with $sm->getServiceLocator() you can retrieve services from that one.

It becomes even more fun if you know that the controller plugin manager and the view helper manager are registered as services in the root service locator. If there is a service where you need to inject a runtime object into a view helper, you can easily do that too. For example the url view helper has the router injected, which is required to assemble urls from a route name.

You can get the controller plugin manager with the key "ControllerPluginManager" from the root SM. The view helper manager is registered with "ViewHelperManager" in the SM. You can get a plugin for example like this:

use MyModule\Service;

return array(
  'service_manager' => array(
    'factories' => array(
      'MyModule\Service\Foo' => function($sm) {
        $service = new Service\Foo;

        $plugins = $sm->get('ViewHelperManager');
$plugin = $plugins->plugin('my-plugin');
        $service->setPlugin($plugin);

        return $service;
      },
    ),
  ),
);

Peering service managers

The concept of peering service managers is quite easy to understand. There is a way for the controller plugin and view helper service managers to load the service from the root service manager without using $sm->getServiceLocator(). This concept is peering, which basically means that the controller plugin service manager tries to fetch the service from the root service manager when it fails to load its own service.

So if you look at above example, you can skip in some occasions the getServiceLocator() method and directly fetch the service. This only holds for controller plugins and view helpers. The reason is obvious. There is a controller service manager for security reasons: you might accidentally create an instance of an object just because you request a special URL. You completely knock down this barrier when you allow the controller service manager to get services by peering. However, for controller plugins and view helpers it could still be worth working with peering:

use MyModule\Controller\Plugin;

return array(
  'controller_plugins' => array(
    'factories' => array(
      'MyModule\Controller\Plugin\Foo' => function($sm) {
        $plugin = new Plugin\Foo;

        $cache = $sm->get('my-cache');
        $plugin->setCache($cache);

        return $plugin;
      },
    ),
  ),
);

The benefit you have is that you simply can ignore getServiceLocator() for plugins and helpers. It makes your code perhaps a bit easier to read. You read my sceptical concerns between the lines: peering is not directly easy to grasp. In above example, the $sm does not hold the service "my-cache", but if you try to get it, you get the cache back. Document this kind of factories very well, because else you will get trouble later on!

Personal preference

In my personal opinion, I like the strict usage of interfaces in my module. I always apply the Zend\ModuleManager\Feature interfaces. I also like the style where all the services are combined into one config file with closures as factories. This helps to scroll through all service keys from one module, without other clutter of route config (from the module config) or autoload config and bootstrap logic (from the module class).

Usually I have besides the module.config.php also a service.config.php in the config/ directory. And I include that file just like the module configuration. The module classes look often like this:

namespace MyModule;

use Zend\Loader;
use Zend\ModuleManager\Feature;
use Zend\EventManager\EventInterface;

class Module implements
    Feature\AutoloaderProviderInterface,
    Feature\ConfigProviderInterface,
    Feature\ServiceProviderInterface,
    Feature\BootstrapListenerInterface
{
    public function getAutoloaderConfig()
    {
        return array(
            Loader\AutoloaderFactory::STANDARD_AUTOLOADER => array(
                Loader\StandardAutoloader::LOAD_NS => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
                ),
            ),
        );
    }

    public function getConfig()
    {
        return include __DIR__ . '/config/module.config.php';
    }

    public function getServiceConfig()
    {
        return include __DIR__ . '/config/service.config.php';
    }

    public function onBootstrap(EventInterface $e)
    {
        // Some logic
    }
}

In the module.config.php I provide my normal config, in the service.config.php all services are grouped together. An example for this type of setup is shown in EnsembleKernel where the service.config.php looks like this. But of course there are many other possibilities where you can tune the setup to your likes.

Comments

Reese

Thanks, this is helpful, and provides what looks like the missing link for what I needed to get unit testing set up.

Gerard

I like the way you wrote `getAutoloaderConfig()` in your last example. The use of constants and `Loader` oppose to `Zend` looks good.

Liz

as a zf1 user I can only say no. i won´t use that shit. go back to your java or whatever world. This is not PHP.

Uncle Fred

@Liz 2006 called and it wants its ZF1 back.

@Jurian Perhaps you could have another row per each service manager called "Registered in root SM as". The Controller plugins manager would have "ControllerPluginManager", the View helpers manager would have "ViewHelperManager" and the Controllers manager would have "ControllersManager".

Matthew Weier O'Phinney

A couple corrections:

First, we're trying to clarify and simplify the differences between the ServiceManagerAware and ServiceLocatorAware interfaces. The former will very likely be removed from usage _internally_ starting in 2.1, and the initializer that seeds it will also be removed. Elsewhere, we'd already standardized on ServiceLocatorAware.

Second, for factories, there are three types of string arguments you can use. First, a string callable (i.e., a function name or a static method name); Second, a string class name of a class implementing the FactoryInterface; Third, a string class name of a class defining __invoke().

Third, in an effort to make the relationship between a plugin manager and the service manager more clear, in my factories, I always call the incoming plugin manager instance either "$plugins" or some other semantically named variable indicating the plugin type (e.g., "helpers", "filters", etc.). This then allows me to call "$services = $plugins->getServiceLocator();", and the naming makes the relationship and differences clear.

Last, while I like the idea of having service configuration as a separate file, it means two places to look. Additionally, for quick factories of a dozen lines or less, having a class-based factory is often overkill; as such, I will define my cacheable service configuration in the main module configuration, and anything needing a closure inside my Module class itself.

All told, though: very nice article!

Jurian Sluiman

@Fred: thanks for the suggestion. I mentioned the ViewHelperManager and ControllerPluginManager now. The controller is called ControllerLoader and I can't come up with a use case for that, but for the sake of clarity I added that row as well.

@Matthew: thanks for the additional notices!

I explicitly did not mention the Service*Aware interfaces since that would make the article even longer. However, I am thinking on making a series of articles about service oriented design and the Service*Aware interfaces must be mentioned somewhere along those posts!

Together with some other fixes I also updated the fact "factories" might also be a string with a FQCN implementing __invoke(). It's something I rarely use and thus also forgot to mention. I also extracted another variable to show $plugins and not directly calling the plugin() method to fetch the appropriate plugin. I hope it is a bit more clear now.

Regarding your last argument: I am rather against that type of usage. First, you introduce two places where services might be defined. You can easily overlook one if you only look at the other list. That can cause you to create a new services while the service already exists.

If you use your module class, it get mixed with all kind of bootstrap logic (which I use a lot for many modules; for example attaching listeners based on configuration flags). If you use the module configuration, you get all kind of mixed configs in there: the view, the router and some navigation or other services. If you combine that with the wiring of your application, that becomes too cluttered as well. As of the third option (factory classes) you split the service name and the behaviour of the wiring. It makes it therefore harder to load other dependencies because you have the wiring at one place and the service naming at another.

The only good option I could find to solve all my (yes, of course it is subjective) "problems", I came up with the service.config.php class with closures. But as I state it in my last sentence: Zend Framework 2 is so extremely flexible, you can make it really like you want. I think there is no right or wrong, just a matter of preference for one above another method.

andy

I'm a zf1 user for several years now, and im disappointed from the directions zf2 is taking. zf2 became an over complexed framework and there's noway for me to upgrade my existing projects.

this is another example for an over complex feature with auto loaders / config files / di that i have no idea where to start with..

Charlie

Cool article!

I'm a beginner using ZF2. this article and Evan's made things a bit more clear. With all articles and blogs mentioning "ServiceManager", no one had bother to explaing this in terms of ZF2 untill now. keep up the good work, Jurian and don't forget the newbies! :-)

Oleg Lobach

Hi Jurian,

I think, you miss 'array' keyword in some code blog. F.e. here:

'controller_plugins' =>
'factories' => array(

(look at part "The root manager versus the others" and "Peering service managers")

This is very cool and usefull post, and I decide translate it to russian, if you don`t mind.

Jurian Sluiman

Hi Oleg, cool to hear you want to translate the article into Russian! Of course it is not a problem to translate it, I have made all my articles available under the Creative Commons Attribution-ShareAlike license: http://creativecommons.org/licenses/by-sa/3.0/

Two three missing "array" keywords have also been added, thanks for notifying me.

Place a comment

If you have a user account for this site, you can login to click here.

Please note your comment below will be removed if you login!

 
 

The address is stored internally but not displayed on this site. We will respect your privacy.

In your message no html is allowd. A blank line creates a new paragraph, an url gets a hyperlink.