Zend Framework 2: Service Manager

  • Tutorial

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
Поделиться публикацией

Похожие публикации

Комментарии 35
    0
    Спасибо, полезно. Отдельно порадовало упоминанию нюанса с замыканиями и кэшированием. Первый раз с этим сталкиваешься, когда уже все написано и дошло дело до оптимизации. И тут начинается утомительный процесс вынесения всех замыканий в отдельные классы.

    Правда для новичков, мне кажется, вы погорячились, слишком многое остается за скобками :)

    Вопрос — инициализаторы вызываются для всех сервисов?
      0
      да.

       public function doCreate($rName, $cName) {
      // ..
              foreach ($this->initializers as $initializer) {
                  if ($initializer instanceof InitializerInterface) {
                      $initializer->initialize($instance, $this);
                  } else {
                      call_user_func($initializer, $instance, $this);
                  }
              }
      }
      
        0
        еще вопрос, что именно за скобками? это нужно учесть и исправить в следующем посте ;)
          0
          Может быть не хватает чуть более подробного введения и описания общего алгоритма, по которому создаются сервисы. «АФ — это последняя попытка СМ создать запрашиваемый сервис.» Вот на этой фразе закрадывается мысль, что наверное Service Locator проходит по всем сервисам в каком-то определенном порядке. Но об этом вы наверное собирались написать во второй части.
          А еще во второй части можно было написать, для чего вообще СМ нужен, про IoC, про вынесение абстракции «наверх» и удобство тестирования, ну и про минусы, разумеется, т.к. Service Locator активно критикуют и в ZF3 вроде намечены какие-то изменения на этот счет.
      0
      Еще вопрос: Как решить вопрос с автодополнеием имен сервисов при обращении к ним? Даже в относительно небольшом проекте сервисов набирается не один десяток, держать каждый из них в голове не получается, приходится каждый раз сверяться с конфигом.
        +1
        По моему — это вопрос правильного наименования, в статье не зря есть «best practices».
        Хорошей практикой является добавления неймспейса модуля к названию сервиса. Либо использовать абсолютное название класса сервиса.


        Я делаю через второй способ:
        // В конфиге:
        namespace User;
        ....
            'service_manager' => [
                'factories'  => [
                    Service\RoleService::class       => Factory\Service\RoleServiceFactory::class,
                ],
            ],
        
        //В контроллере:
        use User\Service\RoleService;
        ......
        public function indexAction() {
            //При вводе Ro -> выскакивает автодополнение IDE для класа RoleService
            // А в PhpStorm даже без указания use вверху (потом после ввода класа шторм сам добавит нужный use)
        
            $roleService = $this->getServiceLocator()->get(RoleService::class);
        
            // А для автодополнения $roleService приходиться указывать PhpDoc @var
        }
        


        Ну естественно конструкция "::class" ограничивает совместимость — только от PHP 5.5.
        Зато удобно, привыкаешь в мгновение ока… )
          0
          Удобно, но нужно ли autoloader'у подгружать каждый класс в конфиге?
            0
            Это извечная борьба между производительностью и удобностью разработки. )

            Ну в конфиге можно не пользоваться "::class" и ручками полное имя вписывать «User\Service\RoleService» (сначала сделать вид, что пишешь имя класса — IDE дополнит его, а потом обернуть в кавычки). И волки сыты, и овцы целы )
              +2
              Все будет хорошо, ::class не дергает автолоадер, это скорее такой макрос, чисто технически просто разворачивает в строку с полным неймспейсом.

              $ php -r 'namespace bla; var_dump(foo\bar::class);'
              string(11) «bla\foo\bar»
                0
                wow. Вот это новость, спасибо. Оказывается даже `use Namespace\Class` не дергает автозагрузчик :)
            0
            use Library\Mvc\Controller;
            
            /**
             * @property \Library\Acl         $acl
             * @property \Library\Session     $session
             * @property \Library\Mvc\Request $request
             */
            abstract class ControllerAbstract extends YourFrameworkControllerAbstract {
            
            }
            


            или

            /**
             * @property \Library\Acl         $acl
             * @property \Library\Session     $session
             * @property \Library\Mvc\Request $request
             */
            interface ServicesDummy {
            
            }
            
            abstract class ControllerAbstract extends YourFrameworkControllerAbstract implements ServicesDummy {
            
            }
            
              0
              При условии, что запрос к сервис-менеджеру пробрасывается через «магический» __get()
                0
                можно все в трейт запихнуть
                /**
                 * @return \Library\Acl
                 */
                protected function getAclService(){}
                //...
                
                  0
                  таким образом и метод можно определить (выдержка из зенда):
                  /**
                   * Abstract controller
                   *
                   * Convenience methods for pre-built plugins (@see __call):
                   *
                   * [....]
                   * @method mixed|null identity()
                   * @method \Zend\Http\Response|array prg(string $redirect = null, bool $redirectToUrl = false)
                   * @method \Zend\Http\Response|array postRedirectGet(string $redirect = null, bool $redirectToUrl = false)
                   * @method \Zend\Mvc\Controller\Plugin\Redirect redirect()
                   * @method \Zend\Mvc\Controller\Plugin\Url url()
                   */
                  abstract class AbstractController
                  
                0
                у меня все просто:
                /* @var $user User */
                $user = $sm->get('User');
                
                  0
                  ну вот я про то, как узнать, что он именно User, а не какой-нибудь doctrine.orm_default.bla-bla-bla
                  Вариант с именем класса сервиса мне показался элегантным ) Ну и именовать надо конечно по общему правилу.
                    0
                    так все же элементарно. заведомо известно, что придет этот тип. как вариант, можно брать интерфейс, абстрактный класс или общий родитель. но я придерживаюсь того, что бы сервис возвращал один единственный тип. считаю, это хорошим тоном и тогда описанная вами ситуация не может существовать ;)
                  0
                  В phpstorm-е есть на этот случай специальный хак для описания метаданных на псевдо-php:

                  confluence.jetbrains.com/display/PhpStorm/PhpStorm+Advanced+Metadata

                  Но я не уверен, что можно его заставить работать для вызовов вида $this->getServiceLocator().
                  0
                  То есть в обьект сервиса будет создан даже, при отсутствии его вызовов? Или в случае с Invokable и Factory СМ все же дождется вызова и тогда создаст обьект?
                    +1
                    ждет вызова, тогда создает. но только не в случае, когда сервис установлен вручную:
                    $sm->setService('a', new A);
                    
                    0
                    Странно как то. Вызывать при каждом запросе кучу кода вида if $service instanceof InterfaceName в случае с инициализаторами.
                    Вообще спасибо вам за статью, до неё я немного переживал что не пробовал Zend Framework толком, всё на Symfony2 сижу. Сейчас я вижу что как минимум компонент Symfony Dependency Injection спроектирован лучше чем аналогичный в Zend.
                      0
                      Не совсем правильный вывод. Здесь речь идет о сервис менеджере, который по-сути реестр с «ручным» DI. Для DI есть более харкорный компонент — framework.zend.com/manual/2.3/en/modules/zend.di.introduction.html который работает через Reflection API или конфиг и там уже настоящая магия. Он более похож на Symfony DI, чем Service Manager.
                        0
                        Тоже хотел заметить, что Service Locator лишь один из паттернов, решающих задачу DI, есть в ZF2 и другой вариант, про который вы упомянули. Но комментатор видимо имел в виду, что те сервисы, которые есть в Symfony более легковесны: symfony.com/doc/current/components/dependency_injection/configurators.html
                          0
                          В Symfony DI контейнер компилируемый — то есть многие из операций выполняются один раз при прогреве кеша (или компиляции контейнера). И в рантайме, например, не используется обход всех инициализаторов, дабы понять какой из них может проинициализировать этот сервис. Контейнер компилируется в очень оптимальный с точки зрения производительности код.
                          Причём ручное DI также можно использовать — но оно будет не быстрое.

                          В Zend DI (ServiceLocator) есть что-то похожее?
                            0
                            Конечно:
                            if (!file_exists(__DIR__ . '/di-definition.php')) {
                               $compiler = new Zend\Di\Definition\Compiler();
                               $compiler->addCodeScannerDirectory(
                                   new Zend\Code\Scanner\ScannerDirectory('path/to/library/My/')
                               );
                               $definition = $compiler->compile();
                               file_put_contents(
                                   __DIR__ . '/di-definition.php',
                                   '<?php return ' . var_export($definition->toArray(), true) . '?>;'
                               );
                            } else {
                               $definition = new Zend\Di\Definition\ArrayDefinition(
                                   include __DIR__
                                   span style= . '/di-definition.php'
                               );
                            }
                            


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

                            Больше информации про Di.
                          0
                          При старте любого PHP-фреймворка вообще происходит каждый раз много всего лишнего, чего не хотелось бы делать при каждом запросе. И уж проверка incanceof едва ли окажется узким местом приложения. На эту тему была тут статья «PHP должен умирать» (кто-то дал неверный перевод «умереть»), в том смысле, что при каждом запросе весь цикл инициализации приходится проходить заново. Кстати, была интересная статья с экспериментом, решающим эту проблему: habrahabr.ru/post/220393/
                          Многие все же выбирают удобство и скорость разработки, а оптимизацией занимаются, когда того требует ситуация.
                            0
                            У таких экспериментов большая проблема — придется отказаться от стандартных средств работы с сетевыми демонами и протоколами (скажем PDO) и писать свои неблокирующие аналоги (начиная с самого низкого уровня — mysql-протокола и т.п.), которые умеют работать в event loop-е. Да, такие наработки частично есть в ReactPHP и PHPDaemon — но тем не менее, это требует отказа от готового кода, стандартных фреймворков и библиотек.

                            Написать что-то специализированное (демон для работы с вебсокетами, скажем) — да, так можно. Портировать крупное приложение — нереально практически.
                          0
                          Напишите еще о последовательности, в которой загружаются сервисы определенные в module.config.php и Module.php.

                          Так, например, если в сервисе используется фабрика для построения навигации, внутри которой хочется проверить авторизацию, а авторизация это тоже сервис, то, в зависимоти от того, где какой сервис определен, зависит — потратите ли вы лишние полчаса на WTF или нет.
                            0
                            оно, в принципе, тут упомянуто. все сервисы, не важно где объявлены, в конфиге или в модуле, сперва будут смержены. т.е. когда нужно будет дернуть авторизацию из навигации, оба сервиса уже будут доступны в СМ.
                              0
                              Я сталкивался с проблемой, которую описал неоднократно.

                              При чем, по моим наблюдениям, так же отличается приоритет сервисов объявленных через фабрику и через замыкание. У замыкания приоритет ниже, они идут после фабрик. Привожу пример

                              Вот рабочий вариант фабрики для навигации, внутри фабрики проверяется авторизация из сервиса Auth.
                              Так работает в Module.php.

                              'index_navigation'                      => function (\Zend\ServiceManager\ServiceManager $sm) {
                              
                                                  $navigationF = new Navigation\Service\IndexNavidationFactory();
                                                  $navigation  = $navigationF->createService($sm);
                              
                                                  return $navigation;
                                              },
                              


                              А вот так уже не работает.
                              'index_navigation' => new Navigation\Service\IndexNavidationFactory(),
                              


                              Выдается ошибка внутри фабрики о том, что не найден Auth сервис.
                              Та же ошибка, если переместить в module.config.php

                              'index_navigation' => 'Application\Navigation\Service\IndexNavigationFactory'
                              


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

                              Где-то на немецком ресурсе я видел схему того в каком порядке все мержится. Вот тут.

                              Именно из-за таких нюансов, раз уж вы взялись писать, я прошу вас написать и о порядке загрузки по русски.
                          0
                          У вас ошибка.

                          СМ реалирует


                          Наверное имелось в виду «реализует».

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое