A free and open-source book on ZF3 for beginners


3.10. Service Manager

You can imagine the web application as a set of services. For example, you can have an authentication service responsible for logging in the site users, entity manager service responsible for accessing the database, event manager service responsible for triggering events and delivering them to event listeners, etc.

In Zend Framework, the ServiceManager class is a centralized container for all application services. The service manager is implemented in Zend\ServiceManager component, as the ServiceManager class. Class inheritance diagram is shown in figure 3.5 below:

Figure 3.5. Service manager class inheritance diagram Figure 3.5. Service manager class inheritance diagram

The service manager is created on application start up (inside of init() static method of Zend\Mvc\Application class). The standard services available through service manager are presented in table 3.1. This table is incomplete, because the actual number of services registered in service manager may be much bigger.

Table 3.1. Standard services
Service Name Description
Application Allows to retrieve the singleton of Zend\Mvc\Application class.
ApplicationConfig Configuration array extracted from application.config.php file.
Config Merged configuration array extracted from module.config.php files merged with autoload/global.php and autoload/local.php.
EventManager Allows to retrieve a new instance of Zend\EventManager\EventManager class. The event manager allows to send (trigger) events and attach event listeners.
SharedEventManager Allows to retrieve the singleton instance of Zend\EventManager\SharedEventManager class. The shared event manager allows to listen to events defined by other classes and components.
ModuleManager Allows to retrieve the singleton of Zend\ModuleManager\ModuleManager class. The module manager is responsible for loading application modules.
Request The singleton of Zend\Http\Request class. Represents HTTP request received from client.
Response The singleton of Zend\Http\Response class. Represents HTTP response that will be sent to client.
Router The singleton of Zend\Router\Http\TreeRouteStack. Performs URL routing.
ServiceManager Service manager itself.
ViewManager The singleton of Zend\Mvc\View\Http\ViewManager class. Responsible for preparing the view layer for page rendering.

A service is typically an arbitrary PHP class, but not always. For example, when ZF3 loads the configuration files and merges the data into nested arrays, it saves the arrays in the service manager as a couple of services (!): ApplicationConfig and Config. The first one is the array loaded from application-level configuration file application.config.php, and the later one is the merged array from module-level config files and auto-loaded application-level config files. Thus, in the service manager you can store anything you want: a PHP class, a simple variable or an array.

From table 3.1, you can see that in ZF3 almost everything can be considered as a service. The service manager is itself registered as a service. Moreover, the Application class is also registered as a service.

An important thing you should note about the services is that they are typically stored in a single instance only (this is also called the singleton pattern). Obviously, you don't need the second instance of the Application class (in that case you would have a nightmare).

But, there is an important exception from the rule above. It may be confusing at first, but the EventManager is not a singleton. Each time you retrieve the event manager service from service manager, you receive a new object. This is done for performance reasons and to avoid possible event conflicts between different components. We will discuss this further in the About Event Manager section later in this chapter.

The service manager defines several methods needed for locating and retrieving a service from the service manager (see the table 3.2 below).

Table 3.2. ServiceManager methods
Method Name Description
has($name) Checks if such a service is registered.
get($name) Retrieves a registered service's instance.
build($name, $options) Always returns a new instance of the requested service.

You can test if a service is registered by passing its name to the service manager's has() method. It returns a boolean true if the service is registered, or false if the service with such a name is not registered.

You can retrieve a service by its name later with the help of the service manager's get() method. This method takes a single parameter representing the service name. Look at the following example:

<?php 

// Retrieve the application config array.
$appConfig = $serviceManager->get('ApplicationConfig');

// Use it (for example, retrieve the module list).
$modules = $appConfig['modules'];

And the build() method always creates a new instance of the service when you call it (comparing to get(), which typically creates the instance of the service only once and returns it on later requests).

You will typically retrieve services from service manager not in any place of your code, but inside of a factory. A factory is a code responsible for creation of an object. When creating the object, you can retrieve services it depends on from the service manager and pass those services (dependencies) to the object's constructor. This is also called dependency injection.

If you have some experience with Zend Framework 2, you may notice that the things are now a little different than before. In ZF2, there was ServiceLocator pattern allowing to get dependencies from service manager in any part of your app (in controllers, services, etc.) In ZF3, you have to pass dependencies explicitly. It is a little more boring, but it removes "hidden" dependencies and makes your code more clear and easier to understand.

3.10.1. Registering a Service

When writing your website, you will often need to register your own service in the service manager. One of the ways to register a service is using the setService() method of the service manager. For example, let's create and register the currency converter service class, which will be used, for example, on a shopping cart page to convert EUR currency to USD:

<?php 
// Define a namespace where our custom service lives.
namespace Application\Service;

// Define a currency converter service class.
class CurrencyConverter 
{
    // Converts euros to US dollars.
    public function convertEURtoUSD($amount) 
    {
        return $amount*1.25;
    }
	
    //...
}

Above, in lines 6-15 we define an example CurrencyConverter class (for simplicity, we implement only a single method convertEURtoUSD() which is able to convert euros to US dollars).

// Create an instance of the class.
$service = new CurrencyConverter();
// Save the instance to service manager.
$serviceManager->setService(CurrencyConverter::class, $service);

In the example above, we instantiate the class with the new operator, and register it with the service manager using the setService() method (we assume that the $serviceManager variable is of type Zend\ServiceManager\ServiceManager class, and that it was declared somewhere else).

The setService() method takes two parameters: the service name string, and the service instance. The service name should be unique within all other possible services.

Once the service is stored in service manager, you can retrieve it by name at any place of your application with the help of the service manager's get() method. Look at the following example:

<?php 
// Retrieve the currency converter service.
$service = $serviceManager->get(CurrencyConverter::class);

// Use it (convert money amount).
$convertedAmount = $service->convertEURtoUSD(50);

3.10.2. Service Names

Different services can use different naming styles. For example, the same currency converter service may be registered under the different names: CurrencyConverter, currency_converter and so on. To introduce some uniform naming convention, it is recommended to register a service by its fully qualified class name, as follows:

$serviceManager->setService(CurrencyConverter::class);

In the example above, we used the keyword class. It is available since PHP 5.5 and is used for class name resolution. CurrencyConverter::class is expanded to the fully qualified name of the class, like \Application\Service\CurrencyConverter.

3.10.3. Overriding an Existing Service

If you are trying to register the service name which is already present, the setService() method will throw an exception. But sometimes you want to override the service with the same name (to replace it by the new one). For this purpose, you can use the setAllowOverride() method of the service manager:

<?php 
// Allow to replace services 
$serviceManager->setAllowOverride(true);

// Save the instance to service manager. There will be no exception
// even if there is another service with such a name.
$serviceManager->setService(CurrencyConverter::class, $service);

Above, the setAllowOverride() method takes the single boolean parameter defining whether to allow you replace the service CurrencyConverter if such a name is already present, or not.

3.10.4. Registering Invokable Classes

What is bad with the setService() method is that you have to create the service instance before you really need it. If you never use the service, the service instantiation will only waste the time and memory. To resolve this issue, the service manager provides you with the setInvokableClass() method.

<?php 
// Register an invokable class
$serviceManager->setInvokableClass(CurrencyConverter::class);

In the example above, we pass to the service manager the fully qualified class name of the service instead of passing its instance. With this technique, the service will be instantiated by the service manager only when someone calls the get(CurrencyConverter::class) method. This is also called lazy loading.

Services often depend on each other. For example, the currency converter service may use entity manager service to read money exchange rates from database. The disadvantage of setInvokableClass() method is that it doesn't allow to pass parameters (dependencies) to the service on object instantiation. To resolve this issue, you can use factories, as described below.

3.10.5. Registering a Factory

A factory is a class that can do only one thing - to create other objects.

You register a factory for a service with the setFactory() method of the service manager:

The simplest factory is InvokableFactory - it is analogous to the setInvokableClass() method from the previous section.

<?php 
use Zend\ServiceManager\Factory\InvokableFactory;

// This is equivalent to the setInvokableClass() method from previous section.
$serviceManager->setFactory(CurrencyConverter::class, InvokableFactory::class);

After you have registered the factory you can retrieve the service from service manager as usual with the get() method. The service will be instantiated only when you retrieve it from service manager (lazy loading).

Sometimes, service instantiation is more complex than just creating the service instance with new operator (like InvokableFactory does). You may need to pass some parameters to the service's constructor or invoke some service methods just after construction. This complex instantiation logic can be encapsulated inside of your own custom factory class. The factory class typically implements the FactoryInterface:

<?php
namespace Zend\ServiceManager\Factory;

use Interop\Container\ContainerInterface;

interface FactoryInterface
{
    public function __invoke(ContainerInterface $container, 
                        $requestedName, array $options = null);
}

As we see from the definition of the FactoryInterface, the factory class must provide the __invoke magic method returning the instance of a single service. The service manager is passed to the __invoke method as the $container parameter; it can be used during the construction of the service for accessing other services (to inject dependencies). The second argument ($requestedName) is the service name. The third argument ($options) can be used to pass some parameters to the service, and is used only when you request the service with the build() method of the service manager.

As an example, let's write a factory for our currency converter service (see the code below). We don't use complex construction logics for our CurrencyConverter service, but for more complex services, you may need to use one.

<?php 
namespace Application\Service\Factory;

use Zend\ServiceManager\Factory\FactoryInterface;
use Application\Service\CurrencyConverter;

// Factory class
class CurrencyConverterFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, 
                     $requestedName, array $options = null) 
    {
        // Create an instance of the class.
        $service = new CurrencyConverter();	
	
        return $service;
    }
}

Technically, in ZF3 you can use the same factory class for instantiating several services that have similar instantiation code (for that purpose, you can use the $requestedName argument passed to the __invoke() method of the factory). However, mostly you will create a different factory per each service.

3.10.6. Registering an Abstract Factory

Even more complex case of a factory is when you need to determine at run time which service names should be registered. For such a situation, you can use an abstract factory. An abstract factory class should implement the AbstractFactoryInterface interface:

<?php 
namespace Zend\ServiceManager\Factory;

use Interop\Container\ContainerInterface;

interface AbstractFactoryInterface extends FactoryInterface
{
    public function canCreate(ContainerInterface $container, $requestedName);
}

An abstract factory has two methods: canCreate() and __invoke(). The first one is needed to test if the factory can create the service with the certain name, and the latter one allows to actually create the service. The methods take two parameters: service manager ($container) and service name ($requestedName).

Comparing to usual factory class, the difference is that the usual factory class typically creates only a single type of service, but an abstract factory can dynamically create as many types of services as it wants.

You register an abstract factory with the setAbstractFactory() method of the service manager.

Abstract factories are a powerful feature, but you should use them only when really necessary, because they negatively impact the performance. It is better to use the usual (non-abstract) factories.

3.10.7. Registering Service Aliases

Sometimes, you may want to define an alias for a service. The alias is like a symbolic link: it references the already registered service. To create an alias, you use the service manager's setAlias() method:

<?php 
// Register an alias for the CurrencyConverter service
$serviceManager->setAlias('CurConv', CurrencyConverter::class);

Once registered, you can retrieve the service by both its name and alias using the service manager's get() method.

3.10.8. Shared and Non-Shared Services

By default, services are stored in service manager in single instance only. This is also called the singleton design pattern. For example, when you try to retrieve the CurrencyConverter service twice, you will receive the same object. This is also called a shared service.

But, in some (rare) situations, you will need to create a new instance of a service each time someone requests it from service manager. An example is the EventManager - you get a new instance of it each time you request it.

To mark a service as a non-shared, you can use service manager's setShared() method:

$serviceManager->setShared('EventManager', false);

3.10.9. Service Manager Configuration

In your website, you typically use service manager configuration to register your services (instead of calling service manager's methods as described above).

To automatically register a service within the service manager, typically the service_manager key of a configuration file is used. You can put this key either inside of an application-level configuration file or in a module-level configuration file.

If you are putting this key in a module-level configuration file, be careful about the danger of name overwriting during the configs merge. Do not register the same service name in different modules.

This service_manager key should look like below:

<?php 
return [
    //...

    // Register the services under this key
    'service_manager' => [
        'services' => [
            // Register service class instances here
            //...
        ],
        'invokables' => [
            // Register invokable classes here
            //...
        ],
        'factories' => [
            // Register factories here
            //...
        ],
        'abstract_factories' => [
            // Register abstract factories here
            //...
        ],
        'aliases' => [
            // Register service aliases here
            //...
        ],
        'shared' => [
            // Specify here which services must be non-shared
        ]
  ],
  
  //...
];

In the example above, you can see that the service_manager key may contain several subkeys for registering services in different ways:

As an example, let's register our CurrencyConverter service and create an alias for it:

<?php 
use Zend\ServiceManager\Factory\InvokableFactory;
use Application\Service\CurrencyConverter;

return [
    //...

    // Register the services under this key
    'service_manager' => [
        'factories' => [
            // Register CurrencyConverter service.
            CurrencyConverter::class => InvokableFactory::class
        ],
        'aliases' => [
            // Register an alias for the CurrencyConverter service.
            'CurConv' => CurrencyConverter::class
        ],        
  ],
  
  //...
];

Top