A free and open-source book on ZF3 for beginners


3.10. Менеджер сервисов

Веб-приложение можно представить как набор сервисов. Например, у вас может быть сервис аутентификации, ответственный за вход пользователей на сайт, менеджер сущностей, ответственный за доступ к базе данных, менеджер событий, ответственный за вызов событий и их обработку, и т.д.

В Zend Framework класс ServiceManager - это централизованный контейнер для всех сервисов приложения. Менеджер сервисов реализован в компоненте Zend\ServiceManager в качестве класса ServiceManager. Диаграмма наследования классов приведена на рис. 3.5 ниже:

Рисунок 3.5. Диаграмма наследования классов менеджера сервисов Рисунок 3.5. Диаграмма наследования классов менеджера сервисов

Менеджер сервисов создается при запуске приложения (внутри статического метода init() класса Zend\Mvc\Application). Стандартные сервисы, доступные через менеджер сервисов, представлены в таблице 3.1. Эта таблица неполная, так как реальное число сервисов, зарегистрированных в менеджере может быть намного больше.

Table 3.1. Standard services
Имя сервиса Описание
Application Позволяет извлечь единственный экземпляр класса Zend\Mvc\Application.
ApplicationConfig Массив конфигурации из файла application.config.php file.
Config Объединенный массив конфигурации из файлов module.config.php, autoload/global.php и autoload/local.php.
EventManager Позволяет извлечь новый экземпляр класса Zend\EventManager\EventManager. Менеджер событий позволяет вызывать события и прикреплять обработчики.
SharedEventManager Позволяет извлечь единственный экземпляр класса Zend\EventManager\SharedEventManager. Общий (shared) менеджер событий позволяет слушать события, определенные в других классах и компонентах.
ModuleManager Позволяет извлечь единственный экземпляр класса Zend\ModuleManager\ModuleManager. Менеджер модулей отвечает за загрузку модулей приложения.
Request Единственный экземпляр класса Zend\Http\Request. Представляет собой HTTP-запрос, полученный от клиента.
Response Единственный экземпляр класса Zend\Http\Response. Представляет собой HTTP-ответ, который будет отправлен клиенту.
Router Единственный экземпляр класса Zend\Router\Http\TreeRouteStack. Осуществляет маршрутизацию URL.
ServiceManager Собственно, сам менеджер сервисов.
ViewManager Единственный экземпляр класса Zend\Mvc\View\Http\ViewManager. Отвечает за подготовку слоя представления к визуализации страницы.

Сервисом обычно является произвольный PHP-класс, но это не всегда так. Например, когда ZF3 загружает файлы конфигурации и объединяет данные во вложенные массивы, он сохраняет массивы в менеджере сервисов как несколько сервисов (!): ApplicationConfig and Config. Первый - массив, загружаемый из файла конфигурации на уровне приложения application.config.php, второй - объединенный массив из файлов конфигурации на уровне модулей и автоматически загруженных файлов конфигурации на уровне приложения. Таким образом, в менеджере сервисов вы можете хранить что хотите: PHP-класс, простую переменную или массив.

Из таблицы 3.1 видно, что в ZF3 все может считаться сервисом - даже сам менеджер сервисов. Более того, класс Application также зарегистрирован, как сервис.

Важная деталь о сервисах, на которую надо обратить внимание: они, как правило, хранятся в единственном экземпляре (это называется шаблоном проектирования одиночка - singleton). Очевидно, вам не нужен второй экземпляр класса Application (в этом случае вам будут гарантированы кошмары).

Однако есть важное исключение из выше приведенного правила. Сначало может показаться непонятным, но сервис менеджера событий EventManager не хранится в единственном экземпляре. Каждый раз как вы извлекаете его из менеджера сервисов, вы получаете новый объект. Это делается с целью улучшения производительности и во избежания возможных конфликтов событий между разными компонентами. Мы обсудим это в разделе О менеджере событий позже в данной главе.

Менеджер сервисов определяет несколько методов, необходимых для нахождения сервиса и его извлечения из менеджера (см. табл. 3.2 ниже).

Таблица 3.2. Методы класса ServiceManager
Имя метода Описание
has($name) Проверяет, зарегистрирован ли такой сервис.
get($name) Извлекает экземпляр зарегистрированного сервиса.
build($name, $options) Всегда создает новый экземпляр запрошенного сервиса.

Вы можете проверить, зарегистрирован ли сервис, передав его имя методу has() менеджера сервисов. Он возвращает true, если сервис зарегистрирован, или false, если сервис с таким именем не зарегистрирован.

Вы можете извлечь сервис по его имени позже с помощью метода get() менеджера сервисов. Этот метод принимает один единственный параметр, представляющий имя сервиса. Взглянем на следующий пример:

<?php 

// Извлекает массив конфигурации приложения.
$appConfig = $serviceManager->get('ApplicationConfig');

// Использует его (например, извлекает список модулей).
$modules = $appConfig['modules'];

И, наконец, метод build() всегда создает новый экземпляр сервиса, когда вы вызываете его (по сравнению с get(), который обычно создает экземпляр сервиса только один раз и возвращает его на все последующие запросы).

Обычно вы будете извлекать сервисы из менеджера сервисов не в любом месте вашего кода, а внутри фабрики (factory). Фабрика - это код, ответственный за создание объектов. Когда вы создаете объект, вы извлечете сервисы, от которых он зависит, и передадите эти сервисы (зависимости) конструктору объекта. Это также называется внедрением зависимостей (dependency injection).

Если у вас есть опыт с Zend Framework 2, вы можете заметить, что все несколько изменилось. В ZF2 применялся шаблон ServiceLocator, позволяющий извлекать зависимости из менеджера сервисов в любой части приложения (в контроллерах, других сервисах, и т.д.) В ZF3 же вам придется передавать зависимости принудительно (explicitly). Это немного более утомительно, но удаляет "скрытые" зависимости и делает ваш код более легким в понимании.

3.10.1. Регистрация сервиса

При написании веб-сайта, вам часто придется регистрировать ваш собственный сервис в менеджере сервисов. Чтобы это сделать, один из способов - вызвать метод setService() менеджера сервисов. Давайте создадим и зарегистрируем класс сервиса конвертера валют, который будет использоваться, например, на странице корзины покупок для конвертации EUR->USD.

<?php 
// Определяем пространство имен, где находится наш сервис.
namespace Application\Service;

// Определяем класс сервиса конвертера.
class CurrencyConverter 
{
    // Конвертирование евро в доллары.
    public function convertEURtoUSD($amount) 
    {
        return $amount*1.25;
    }
	
    //...
}

В строках 6-15 мы определяем класс CurrencyConverter (для упрощения он будет иметь только один метод - метод конвертации валют convertEURtoUSD()).

// Создаем экземпляр класса.
$service = new CurrencyConverter();
// Сохраняем его в менеджере сервисов.
$serviceManager->setService(CurrencyConverter::class, $service);

В примере выше мы инстанцируем класс с помощью оператора new и регистрируем его с помощью метода менеджера сервисов setService() (мы полагаем, что переменная $serviceManager - типа класса Zend\ServiceManager\ServiceManager и была объявлена где-то еще).

Метод setService() принимает два параметра: строку с именем сервиса и экземпляр сервиса. Имя сервиса должно быть уникальным среди имен всех возможных сервисов.

Как только сервис помещается в менеджер сервисов, вы можете извлечь его позже с помощью метода get(). Посмотрим на следующий пример:

<?php 
// Извлекает менеджер конвертации валют.
$service = $serviceManager->get(CurrencyConverter::class);

// Использует его (конвертирует средства).
$convertedAmount = $service->convertEURtoUSD(50);

3.10.2. Имена сервисов

Разные сервисы могут использовать разные стили именования. Например, один и тот же сервис конвертации валюты может быть зарегистрирован под разными именами: CurrencyConverter, currency_converter и так далее. Чтобы применить некоторые правила формирования имен, рекомендуется регистрировать сервис, используя его полностью определенное имя, следующим образом:

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

В примере выше мы используем ключевое слово class. Оно появилось в версии PHP 5.5 и используется для разрешения имен классов. Имя CurrencyConverter::class расширено до полностью определенного, как \Application\Service\CurrencyConverter.

3.10.3. Переопределение существующего сервиса

Если вы попытаетесь зарегистрировать имя сервиса, которое уже существует, менеджер сервисов выбросит исключение. Однако иногда вы хотите переопределить сервис с таким же именем (чтобы заменить его новым). Для этого вы можете использовать метод setAllowOverride() менеджера сервисов:

<?php 
// Позволяет заменять сервисы 
$serviceManager->setAllowOverride(true);

// Сохраняет экземпляр в менеджер сервисов. Исключения не будет,
// даже если есть другой сервис с таким именем.
$serviceManager->setService(CurrencyConverter::class, $service);

В коде выше метод setAllowOverride() берет один булевый параметр, определяющий, разрешать ли замену сервиса CurrencyConverter, если такое имя уже существует, или нет.

3.10.4. Регистрация вызываемых (invokable) классов

Минус метода setService() в том, что необходимо создавать экземпляр сервиса до того, как он на самом деле понадобится. Если вы не используете сервис вообще, инстанцирование сервиса будет лишь тратой времени и памяти. Чтобы решить эту проблему, менеджер сервисов предоставляет метод setInvokableClass().

<?php 
// Регистрация вызываемого класса
$serviceManager->setInvokableClass(CurrencyConverter::class);

В этом примере мы передаем менеджеру сервисов полностью определенное имя сервиса вместо того, чтобы передавать его экземпляр. Таким образом, сервис будет инстанцирован менеджером только тогда, когда кто-нибудь вызовет метод get(CurrencyConverter::class). Это также называется ленивой загрузкой.

Сервисы часто зависят друг от друга. Например, сервис конвертера валюты может использовать сервис менеджера сущностей (entity manager), чтобы прочитать курсы валют из базы данных. Недостаток метода setInvokableClass() - то, что он не позволяет передавать параметры (зависимости) сервису во время его инстанцирования. Чтобы разрешить эту проблему, вы можете использовать фабрики (factories), как описано ниже.

3.10.5. Регистрация фабрики

Фабрика (factory) - это класс, который может делать только одну вещь - создавать другие объекты других классов.

Вы регистрируете фабрику для сервиса с помощью метода setFactory() менеджера сервисов:

Самая простая фабрика - это фабрика InvokableFactory, она аналогична вызову метода setInvokableClass() из предыдущей секции.

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

// Это эквивалентно вызову метода setInvokableClass() из предыдущего раздела.
$serviceManager->setFactory(CurrencyConverter::class, InvokableFactory::class);

После того как вы зарегистрировали фабрику, вы можете извлечь сервис из менеджера сервисов как обычно методом get(). Сервис будет инстанцирован в момент извлечения (ленивая загрузка).

Иногда инстанцирование сервиса может быть сложнее, чем просто создание экземпляра сервиса через оператор new (как делает фабрика InvokableFactory). Вам, возможно, придется передать некоторые параметры конструктору сервиса или вызвать некоторые методы сервиса сразу после его создания. Эти сложные алгоритмы инстанцирования могут быть инкапсулированы в вашу собственную фабрику. Как правило, класс фабрики реализует FactoryInterface:

<?php
namespace Zend\ServiceManager\Factory;

use Interop\Container\ContainerInterface;

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

Как видно из определения FactoryInterface, фабричный класс должен предоставлять магический метод __invoke , возвращающий экземпляр одного сервиса. Менеджер сервисов передается методу __invoke как параметр $container; он может использоваться при создании сервиса для доступа к другим сервисам (для внедрения зависимостей). Второй аргумент ($requestedName) - это имя сервиса. Третий аргумент ($options) может использоваться для передачи параметров сервису, и используется только когда вы извлекаете сервис с помощью метода build() менеджера сервисов.

Для примера давайте напишем фабрику для нашего сервиса конвертации валют (см. код ниже). Мы не будем использовать сложные алгоритмы создания сервиса CurrencyConverter, но для более тяжелых сервисов, вам, возможно, они понадобятся.

<?php 
namespace Application\Service\Factory;

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

// Класс фабрики
class CurrencyConverterFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, 
                     $requestedName, array $options = null) 
    {
        // Создание экземпляра класса.
        $service = new CurrencyConverter();	
	
        return $service;
    }
}

Технически, в ZF3 вы можете использовать один и тот же класс фабрики для инстанциации нескольких сервисов, которые имеют схожий код инстанциации (для этой цели вы можете использовать аргумент $requestedName, передаваемый методу __invoke() фабрики). Однако, в большинстве случаев вы будете создавать новую фабрику для каждого отдельного сервиса.

3.10.6. Регистрация абстрактной фабрики

Возможен еще более сложный случай фабрики - когда вам нужно определить, какие имена сервисов должны быть зарегистрированы во время выполнения. В этой ситуации вы можете использовать абстрактную фабрику. Класс абстрактной фабрики должен определять интерфейс AbstractFactoryInterface:

<?php 
namespace Zend\ServiceManager\Factory;

use Interop\Container\ContainerInterface;

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

Абстрактная фабрика имеет два метода: canCreate() и __invoke(). Первый нужен для проверки того, может ли фабрика создать сервис с определенным именем. Второй позволяет, собственно, создать сервис. Оба метода принимают два параметра: менеджер сервисов ($container) и имя сервиса ($requestedName).

По сравнению с обычным классом фабрики, разница состоит в том, что обычный класс обычно создает только один сервис, в то время как абстрактный может динамически создать столько сервисов, сколько необходимо.

Вы можете зарегистрировать абстрактную фабрику с помощью метода setAbstractFactory() менеджера сервисов.

Абстрактные фабрики - это очень мощная вещь, но их нужно использовать только когда очень нужно, поскольку они негативно влияют на производительность. Лучше использовать обычные (не абстрактные) фабрики.

3.10.7. Регистрация псевдонима сервиса

Иногда вам понадобится создать псевдоним для сервиса. Псевдоним похож на символьную ссылку: он ссылается на уже существующий сервис. Для создания псевдонима используется метод менеджера сервисов setAlias():

<?php 
// Регистрируем псевдоним для сервиса CurrencyConverter
$serviceManager->setAlias('CurConv', CurrencyConverter::class);

После регистрации псевдонима вы можете извлечь сервис и по его имени, и по псевдониму, используя метод get().

3.10.8. Общие (shared) и необщие (non-shared) сервисы

По умолчанию сервисы хранятся в менеджере сервисов в одном экземпляре (шаблон проектирования singleton). Например, когда вы два раза подряд извлекаете сервис CurrencyConverter, вы получите один и тот же объект. Это называется общим (shared) сервисом.

Но в некоторых (редких) случаях, вам может понадобиться создавать новый экземпляр сервиса каждый раз как кто-нибудь извлекает его из менеджера сервисов. Пример - сервис менеджера событий EventManager - вы получите новый экземпляр каждый раз как вы извлекаете его.

Чтобы пометить сервис как non-shared, вы можете воспользоваться методом setShared() менеджера сервисов:

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

3.10.9. Конфигурация менеджера сервисов

В вашем веб-сайте, вы будете обычно регистрировать сервисы в файле конфигурации (вместо вызова методов сервис-менеджера, как было уже показано выше).

Для автоматической регистрации сервиса в менеджере, как правило, используется ключ service_manager файла конфигурации. Вы можете поместить этот ключ либо в файл конфигурации на уровне приложения, либо файл конфигурации на уровне модуля.

Если вы поместили этот ключ в файл конфигурации на уровне модуля, помните об опасности перезаписи имени во время слияния конфигураций. Не регистрируйте одинаковые имена сервисов в разных модулях.

Ключ service_manager должен выглядеть так:

<?php 
return [
    //...

    // Регистрация сервисов под этим ключом
    'service_manager' => [
        'services' => [
            // Регистрация экземпляров классов сервисов
            //...
        ],
        'invokables' => [
            // Регистрация вызываемых классов
            //...
        ],
        'factories' => [
            // Регистрация фабрик
            //...
        ],
        'abstract_factories' => [
            // Регистрация абстрактных фабрик
            //...
        ],
        'aliases' => [
            // Регистрация псевдонимов сервисов
            //...
        ],
        'shared' => [
            // Укажите какие сервисы должны быть non-shared
        ]
    ],
  
    //...
];

Как видите в примере выше, ключ service_manager может содержать несколько подключей для регистрации сервисов по-разному:

В качестве примера давайте зарегистрируем наш сервис CurrencyConverter и псевдоним для него:

<?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