Pull to refresh

Zend Framework 2: Service Manager

Reading time 7 min
Views 21K

Service Manager (SM, CM) в ZF2.


Service Manager — это один из ключевых компонентов Zend Framework 2, который существенно облегчает жизнь разрабочика избавляя его от дублирования кода и рутинных операций по созданию и настройки сервисов, позволяя их конфигурировать на максимально высоком уровне. СМ, по своей натуре, является реестром сервисов, основная задача которого — создание и хранение сервисов. Можно сказать, СМ является очень продвинутой версий компонента Zend_Registry из Zend Framework 1.
СМ реализует паттерн Service Locator. Во многих частях приложения (например, в AbstractActionController) можно встретить функции getServiceLocator(), которые возвращают класс Zend\ServiceManager\ServiceManager. Такое несоответствие названия метода и возвращаемого типа легко объясняется тем, что getServiceLocator() возвращает объект, реализующий интерфейс ServiceLocatorInterface:

namespace Zend\ServiceManager;

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

Zend\ServiceManager\ServiceManager как таким и является. Сделано это потому, что в самом фреймворке используется несколько других типов СМ и, кстати, никто не запрещает нам использовать свой собственный сервис менеджер в приложении.

Сервисы.


Сервис — это обычная переменная абсолютно произвольного типа (не обязательно объект, см. сравнение с Zend_Registry):

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

выведет:
array (
    a => 'b'
)

Конфигурация СМ


Настроить сервис менеджет можно четырьмя путями:
1. Через конфиг модуля (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. определив метод getServiceConfig() (для красоты кода можно еще добавить интерфейс Zend\ModuleManager\Feature\ServiceProviderInterface), который вернет массив или Traversable в формате из пункта 1;

3. создав сервис руками и вставив его в СМ:
// IndexController::indexAction()
$arrayService = array('a' => 'b');
$this->getServiceLocator()->setService('arrayService', $arrayService);

4. описав сервис в application.config.php в формате из п. 1.

Нужно помнить, что названия сервисов должны быть уникальными для всего приложения (если, конечно, не стоит цель переопределить существующий сервис). Во время инициализации приложения, Zend\ModuleManager\ModuleManager объединит все конфиги в один, затирая дублирующиеся ключи. Хорошей практикой является добавления неймспейса модуля к названию сервиса. Либо использовать абсолютное название класса сервиса.

Создание сервисов через СМ.


Объекты\простые типы

Самый простой тип. Для создания такого сервиса необходимо просто вручную создать объект (массив, строку, ресурс, т.д.) и передать его в СМ:
$myService = new MyService();
$serviceManager->setService(‘myService’, $myService);

либо через конфиг:
array(
    'service_manager' => array(
        'services' => array(
            'myService' => new MyService()
        )
    )
);

$serviceManager->setService($name, $service) положит объект напрямую во внутреннюю переменную ServiceManager::$instances, которая хранит все проинициализированные сервисы. При обращении к такому типу, СМ не будет пытаться его создать и отдаст как есть
Используя такой тип можно хранить произвольные данные, которые будут доступны во всем приложении (как было с Zend_Registry).

Invokable

Для создания необходимо передать менеджеру полное название целевого класса. СМ создаст его используя оператор new.

// 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);

либо через конфиг:
array(
    'service_manager' => array(
        'invokables' => array(
            'myService' => 'MyService'
        )
    )
);

Применение: если просто нужно создать класс без прямых зависимостей через СМ.
При этом, делегаторы и instantiators все же будут вызваны и внедрят зависимости при необходимости.

Фабрики.

Сервисы могут быть созданы и сконфигурированы в фабрике. Фабрики могут быть двух типов: замыкание и класс, реализующий Zend\ServiceManager\FactoryInterface.

Реализация через замыкание:
array(
    'service_manager' => array(
        'factories' => array(
            'myService' => function (ServiceLocator $serviceManager) {
                return new MyService();
            }
        )
    )
);

Такое подход хоть и сокращает количество строк кода, но хранит в себе подводный камень: замыкания не могут быть корректно сериализированны в строку.
Рельный пример: если в application.config.php включить кеширование объединенного конфига, то при следующем запуске приложение не сможет его скомпилировать и упадет с ошибкой: Fatal error: Call to undefined method Closure::__set_state() in /data/cache/module-config-cache..php

Что бы избежать таких проблем, сервисы нужно создавать через классы-фабрики, которые реализуют Zend\ServiceManager\FactoryInterface:
// Appliction/Service/ConfigProviderFactory.php
class ConfigProviderFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        return new ConfigProvider($serviceLocator->get('Configuration'));
    }
}

и прописаны в конфиге:
array(
    'service_manager' => array(
        'factories' => array(
            'ConfigProvider' => 'ConfigEx\Service\ConfigProviderFactory',
        )
    )
);

Также объект фабрики или название класса можно передать напрямую в СМ:

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

Применение: если нужно создать сервис, который зависит от других сервисов либо нуждается в настройке.

Абстрактные Фабрики

АФ — это последняя попытка СМ создать запрашиваемый сервис. Если СМ не может найти сервис, то начнет опрашивать все зарегистрированные АФ (вызывать метод canCreateServiceWithName()). Если АФ вернет утвердительный ответ, то СМ вызовет метод createServiceWithName() из фабрики, делегируя создание сервиса на логику АФ.

Передача АФ напрямую:
$serviceManager->addAbstractFactory(new AbstractFactory);

addAbstractFactory принимает объект, а не класс!
Настройка через конфиг:
array(
    'service_manager' => array(
        'abstract_factories' => array(
            'DbTableAbstractFactory' => 'Application\Service\‘DbTableAbstractFactory'
        )
    ),

И класс фабрики:
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'));
    }
}

Затем, можно попросить СМ создать нам 2 сервиса:
$serviceManager->get('UserTable');
$serviceManager->get('PostTable');

В результате, будет 2 объекта, которые не были описаны ни в одном из типов сервисов.
Это очень удобная штука. Но на мое мнение, такое поведение не очень предсказуемое для других разработчиков, поэтому использовать нужно с умом. Кому понравится потратить много времени на дебаг магии, которая создает объекты из ничего?

Алиасы


Это просто псевдонимы для других сервисов.
array(
    'service_manager' => array(
        'aliases' => array(
            'myservice' => 'MyService'
        )
    )
);
$serviceLocator->get('myservice') === $serviceLocator->get('MyService'); // true

А теперь перейдем к другим вкусняшкам.

Инициализаторы.


Это уже не сервисы, а фичи самого СМ. Позволяют провести дополнительную инициализацию сервис уже после того, как объект был создан. С их помощью можно реализовать Interface Injection.
Итак, после того, как СМ создал новый объект, он перебирает все зарегистрированные инициализаторы, передавая им объект для последнего шага настройки.

Регистрируются похожим путем, как и фабрики:
Через замыкание:
array(
    'service_manager' => array(
        'initializers' => array(
            'DbAdapterAwareInterface' => function ($instance, ServiceLocator $serviceLocator) {
                if ($instance instanceof DbAdapterAwareInterface) {
                    $instance->setDbAdapter($serviceLocator->get('DbAdapter'));
                }
            }
        )
    )
);

Через класс:
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'
        )
    )
);

В этом примере реализован Interface Injection. Если $instance типа DbAdapterAwareInterface, то инициализатор передаст объекту адаптер БД.

Применение: Interface Injection, донастройка объекта.
Важно знать, что СМ будет для каждого созданного объекта будет вызывать все инициализаторы, что может привести к потере производительности.

Делегаторы.

Делегаторы похожи на инициализаторы, отличие лишь в том, что они будут вызваны для конкретного сервиса, а не для всех подряд.

Регистрация:
array(
    'service_manager' => array(
        'delegators' => array(
            'Router' => array(
                'AnnotatedRouter\Delegator\RouterDelegatorFactory'
            )
        )
    )
);

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

	$service->doSomeCoolStuff(); // то, ради чего создавался делегатор
	// другой код инициализации
	return $service;
    }
}

В этом примере, делегатор RouterDelegatorFactory применяется только на сервис Route.

Применение: дополнительная настройка объекта, полезно для донастройки сервисов из сторонних модулей. Например, в моем модуле для роутинга через аннотации, я использовал делегатор для добавления роутов в стандартный роутер. Был вариант зарегистрировать подписчика EVENT_ROUTE в Module.php с приоритетом выше, чем у стандартного слушателя. Но оно как-то грязновато выглядит…

Shared сервисы.


По умолчанию, СМ создает только один инстанс объекта, при каждом последующем обращении, будет возвращаться один и тот же объект (такой вот синглтон). Что бы запретить это поведение это поведение глобально, нужно вызвать метод setShareByDefault(false). Так же можно отключать такое поведение для определенных сервисов используя конфиг:
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
Tags:
Hubs:
+8
Comments 35
Comments Comments 35

Articles