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:
- What are the different service managers?
- For what reason are different managers used?
- How does the service locator relate to the service manager?
- How can you define services for all those service managers?
- 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:
- General application services (“root service manager” or “main service manager”)
- Controllers
- Controller plugins
- 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 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 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.