Zend Framework 2: Service Manager

  • Tutorial

Service Manager (SM, CM) in ZF2.


Service Manager is one of the key components of Zend Framework 2, which greatly simplifies the life of a developer by eliminating code duplication and routine operations for creating and configuring services, allowing them to be configured at the highest level. SM, by nature, is a registry of services whose main task is the creation and storage of services. We can say that CM is a very advanced version of the Zend_Registry component from Zend Framework 1.
CM implements the Service Locator pattern. In many parts of the application (for example, in AbstractActionController), you can find getServiceLocator () functions that return the Zend \ ServiceManager \ ServiceManager class. This discrepancy between the method name and the return type is easily explained by the fact that getServiceLocator () returns an object that implements the ServiceLocatorInterface interface:

namespace Zend\ServiceManager;
interface ServiceLocatorInterface
{
    public function get($name);
    public function has($name);
}

Zend \ ServiceManager \ ServiceManager as it is. This is done because the framework itself uses several other types of SM and, by the way, no one forbids us to use our own service manager in the application.

Services.


A service is a regular variable of an absolutely arbitrary type (not necessarily an object, see the comparison with Zend_Registry):

// IndexController::indexAction()
$arrayService = array('a' => 'b');
$this->getServiceLocator()->setService('arrayService', $arrayService);
$retrievedService = $this->getServiceLocator()->get('arrayService');
var_dump($retrievedService);
exit;

will output:
array (
    a => 'b'
)

CM configuration


There are four ways to configure the service manager:
1. Through the module config (module.config.php):
return array(
    'service_manager' => array(
        'invokables' => array(),
        'services' => array(),
        'factories' => array(),
        'abstract_factories' => array(),
        'initializators' => array(),
        'delegators' => array(),
        'shared' => array(),
        'aliases' => array()
    )
);

2. Defining the getServiceConfig () method (for the beauty of the code, you can also add the Zend \ ModuleManager \ Feature \ ServiceProviderInterface interface), which will return an array or Traversable in the format from step 1;

3. creating a service by hand and inserting it into the CM:
// IndexController::indexAction()
$arrayService = array('a' => 'b');
$this->getServiceLocator()->setService('arrayService', $arrayService);

4. describing the service in application.config.php in the format of clause 1.

It must be remembered that the names of the services must be unique for the entire application (unless, of course, the goal is to redefine the existing service). During application initialization, Zend \ ModuleManager \ ModuleManager will merge all configs into one, overwriting duplicate keys. It’s good practice to add a namespace module to the name of the service. Or use the absolute name of the class of service.

Creation of services through SM.


Objects \ Simple Types

The simplest type. To create such a service, you just need to manually create an object (array, string, resource, etc.) and transfer it to the CM:
$myService = new MyService();
$serviceManager->setService(‘myService’, $myService);

either through the config:
array(
    'service_manager' => array(
        'services' => array(
            'myService' => new MyService()
        )
    )
);

$ serviceManager-> setService ($ name, $ service) will put the object directly into the internal variable ServiceManager :: $ instances, which stores all the initialized services. When accessing this type, SM will not try to create it and give it as is.
Using this type, you can store arbitrary data that will be available throughout the application (as it was with Zend_Registry).

Invokable

To create, you need to transfer the full name of the target class to the manager. CM will create it using the new operator .

// ServiceManager::createFromInvokable()
protected function createFromInvokable($canonicalName, $requestedName)
{
    $invokable = $this->invokableClasses[$canonicalName];
    if (!class_exists($invokable)) {
        // cut
    }
    $instance = new $invokable;
    return $instance;
}

$myService = new MyService();
$serviceManager->setInvokableClass(‘myService’, $myService);

either through the config:
array(
    'service_manager' => array(
        'invokables' => array(
            'myService' => 'MyService'
        )
    )
);

Application: if you just need to create a class without direct dependencies via CM.
At the same time, delegators and instantiators will nevertheless be called and implement dependencies if necessary.

Factories.

Services can be created and configured in the factory. Factories can be of two types: closure and class that implements Zend \ ServiceManager \ FactoryInterface.

Closed loop implementation:
array(
    'service_manager' => array(
        'factories' => array(
            'myService' => function (ServiceLocator $serviceManager) {
                return new MyService();
            }
        )
    )
);

Although this approach reduces the number of lines of code, it has a pitfall: closures cannot be correctly serialized to a string.
Real-world example: if you enable caching of the combined config in application.config.php, then the next time you start the application, it will not be able to compile it and will crash with an error: Fatal error: Call to undefined method Closure :: __ set_state () in / data / cache / module- config-cache..php

To avoid such problems, services must be created through factory classes that implement Zend \ ServiceManager \ FactoryInterface:
// Appliction/Service/ConfigProviderFactory.php
class ConfigProviderFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        return new ConfigProvider($serviceLocator->get('Configuration'));
    }
}

and are registered in a config:
array(
    'service_manager' => array(
        'factories' => array(
            'ConfigProvider' => 'ConfigEx\Service\ConfigProviderFactory',
        )
    )
);

Also, the factory object or class name can be transferred directly to the CM:

$serviceManager->setFactory('ConfigProvider', new ConfigEx\Service\ConfigProviderFactory());

Application: if you need to create a service that depends on other services or needs to be configured.

Abstract Factories

AF is CM’s last attempt to create the requested service. If the CM cannot find the service, it will start polling all registered AFs (call the canCreateServiceWithName () method). If the AF returns an affirmative answer, then the SM will call the createServiceWithName () method from the factory, delegating the creation of the service to the AF logic.

Direct AF Transfer:
$serviceManager->addAbstractFactory(new AbstractFactory);

addAbstractFactory accepts an object, not a class!
Setting through the config:
array(
    'service_manager' => array(
        'abstract_factories' => array(
            'DbTableAbstractFactory' => 'Application\Service\‘DbTableAbstractFactory'
        )
    ),

And factory class:
class DbTableAbstractFactory implements \Zend\ServiceManager\AbstractFactoryInterface
{
    public function canCreateServiceWithName(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        return preg_match('/Table$/', $name);
    }
    public function createServiceWithName(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        $table =  new $name($serviceLocator->get('DbAdapter'));
    }
}

Then, you can ask SM to create 2 services for us:
$serviceManager->get('UserTable');
$serviceManager->get('PostTable');

As a result, there will be 2 objects that were not described in any of the types of services.
This is a very convenient thing. But in my opinion, this behavior is not very predictable for other developers, so you need to use it wisely. Who would like to spend a lot of time debugging magic that creates objects from nothing?

Aliases


These are just aliases for other services.
array(
    'service_manager' => array(
        'aliases' => array(
            'myservice' => 'MyService'
        )
    )
);
$serviceLocator->get('myservice') === $serviceLocator->get('MyService'); // true

Now let's move on to other snacks.

Initializers.


These are no longer services, but features of the SM itself. They allow for additional initialization of the service after the object has been created. With their help, you can implement Interface Injection.
So, after the SM created a new object, it iterates over all registered initializers, passing them the object for the last configuration step.

Logged in a similar way, like factories:
Through closure:
array(
    'service_manager' => array(
        'initializers' => array(
            'DbAdapterAwareInterface' => function ($instance, ServiceLocator $serviceLocator) {
                if ($instance instanceof DbAdapterAwareInterface) {
                    $instance->setDbAdapter($serviceLocator->get('DbAdapter'));
                }
            }
        )
    )
);

Through the class:
class DbAdapterAwareInterface implements \Zend\ServiceManager\InitializerInterface
{
    public function initialize($instance, \Zend\ServiceManager\ServiceLocatorInterface $serviceLocator)
    {
        if ($instance instanceof DbAdapterAwareInterface) {
                $instance->setDbAdapter($serviceLocator->get('DbAdapter'));
        }
    }
}
array(
    'service_manager' => array(
        'initializers' => array(
            'DbAdapterAwareInterface' => 'DbAdapterAwareInterface'
        )
    )
);

This example implements Interface Injection. If $ instance is of type DbAdapterAwareInterface, then the initializer will pass the database adapter to the object.

Application: Interface Injection, object tuning.
It is important to know that the CM will call all initializers for each created object, which can lead to loss of performance.

Delegators.

Delegators are similar to initializers, the only difference is that they will be called for a specific service, and not for everyone in a row.

Registration:
array(
    'service_manager' => array(
        'delegators' => array(
            'Router' => array(
                'AnnotatedRouter\Delegator\RouterDelegatorFactory'
            )
        )
    )
);

And implementation:
class RouterDelegatorFactory implements DelegatorFactoryInterface
{
    public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback)
    {
        // на этом этапе, целевой сервис еще не создан, создастся они после того, как $callback будет выполнена.
	$service = $callback();
	// сервис уже создан, инициализаторы отработали
	$service->doSomeCoolStuff(); // то, ради чего создавался делегатор
	// другой код инициализации
	return $service;
    }
}

In this example, the RouterDelegatorFactory delegator applies only to the Route service.

Application: additional configuration of the object, useful for reconfiguring services from third-party modules. For example, in my module for routing via annotations, I used a delegator to add routes to a standard router. There was an option to register the EVENT_ROUTE subscriber in Module.php with a priority higher than that of a standard listener. But it somehow looks dirty ...

Shared services.


By default, SM creates only one instance of an object, with each subsequent call, the same object will be returned (such a singleton). To prohibit this behavior this behavior globally, you need to call the setShareByDefault (false) method. You can also disable this behavior for certain services using the config:
array(
    'service_manager' => array(
        'shared' => array(
            'MyService' => false
        )
    )
);
$a = $serviceManager->get('MyService');
$b = $serviceManager->get('MyService');
spl_object_hash($a) === spl_object_hash($b); // false

Also popular now: