Zend Framework 2: Service Manager. Частина 1

Service Manager (SM, CM) в ZF2.

Service Manager — це один з ключових компонентів Zend Framework 2, який суттєво полегшує життя розробник позбавляючи його від дублювання коду і рутинних операцій по створенню і налаштування сервісів, дозволяючи налаштувати на максимально високому рівні. СМ, по своїй натурі, є реєстром сервісів, основне завдання якого — створення і зберігання сервісів. Можна сказати, СМ є дуже просунутій версії компонента Zend_Registry з Zend Framework 1.
СМ реалирует патерн Locator Service. У багатьох частинах додатка (наприклад, в 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

Частина 2.

У наступній частині поговоримо про те, як СМ створює сервіси, про lazy сервісах, які стандартні сервіс менеджери є з коробки зенд і що таке пиринговый сервіс менеджер і напишемо свій власний СМ ;)
Ви можете розширити теми для другої частини, просто написавши про це в коментах з позначкою «частина 2».

— Пост написаний як для новачків, так і для досвідчених розробників.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.