Using Zend Framework service managers in your application

https://juriansluiman.nl/article/120/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 recommendthis 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 lists 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 thegetServiceConfig() 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 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 ervice 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 theservice.config.phplooks like this. But of course there are many other possibilities where you can tune the setup to your likes.







你可能感兴趣的:(zendframework2,service-manager)