Symfony2 Dependency Injection в разрезе из песочницы
Из статьи можно узнать как стартует и работает приложение Symfony2. Мне бы хотелось продолжить цикл статей про этот современный фреймворк и уделить более пристальное внимание такому компоненту как Dependency Injection (DI — внедрение зависимости) так же известный как Service Container.Предисловие
Хотелось бы сначала вкратце описать про архитектуру Symfony2. Ядро приложения состоит из компонентов (Component), которые являются независимыми между собой элементами и выполняют определенные функции. Бизнес-логика приложения заключена в т.н. бандлах. Наравне со встроенными компонентами Symfony2 можно подключить любые другие компоненты-библиотеки сторонних вендоров (в т.ч. популярный Zend), не забыв их правильно зарегистрировать в автолоадере. Как правило, вместе с ядром Symfony2, поставляются такие компоненты как Twig (шаблонизатор), Doctrine2 (ORM), SwiftMailer (mailer).
Сервисно-ориентированная архитектура
Идеология разделения функций на модули, которые выделяются в независимые сервисы, принято называть сервисно-ориентированной архитектурой (Service-oriented architecture, SOA). Она положена в основу Symfony2.
Dependency Injection и Inversion of Control
В приложении с использованием ООП разработчик оперирует и работает с объектами. Каждый объект нацелен на выполнение определенных функций (сервис) и не исключено, что внутри него инкапсулируются другие объекты. Получается зависимость одного объекта от другого, в результате которой родительскому объекту предстоит управлять состоянием экземпляров потомков. Шаблон внедрение зависимости (Dependency Injection, DI) призван избавиться от такой необходимости и предоставить управление зависимостями внешнему коду. Т.е. объект всегда будет работать с готовым экземпляром другого объекта (потомка) и не будет знать как этот объект создается, кем и какие еще зависимости существуют. Родительский объект просто предоставляет механизм подстановки зависимого объекта, как правило, через конструктор или сеттер-метод. Такая передача управления называется Inversion of Control (инверсия управления). Инверсия состоит в том, что сам объект уже не управляет состоянием своих объектов-потомков.
Компонент Dependency Injection в Symfony2 опирается на контейнер, управляет всеми зарегистрированными сервисами и отслеживает связи между ними, создает экземпляры сервисов и использует механизм подстановки.
IoC контейнер
Компоненту DI необходимо знать зависимости между объектами-сервисами, а также какими сервисами он может управлять. Для этого в Symfony2 есть ContainerBuilder, который формируется на основании xml-карты или прямого формирования зависимостей в бандле. Как это происходит в Symfony2. Допустим, в приложении есть App\HelloBundle. Чтобы сформировать контейнер и дополнить его своими сервисами (на уровне фреймворка контейнер уже существует и заполнен сервисами, определенными в стандартных бандлах), необходимо создать директорию DependencyInjection в корневой директории бандла и переопределить метод load класса \Symfony\Component\HttpKernel\DependencyInjection\Extension (согласно правилам Symfony2 класс должен называться AppHelloBundleExtension, т.е. [namespace][название бандла]Extension).
# App\HelloBundle\DependencyInjection\AppHelloBundleExtension.php
<?php
namespace App\HelloBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class AppHelloBundleExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
...
}
}
Сервисы приложения
После того, когда у вас уже есть AppHelloBundleExtension, вы можете начать добавлять свои сервисы. Необходимо учесть, что в данном случае вы оперируете не самими объектами-сервисами, а только лишь их определениями (Definition). Потому что в данном контексте контейнер как таковой еще отсутствует, он лишь формируется на основании определений.
# App\HelloBundle\DependencyInjection\AppHelloBundleExtension.php
public function load(array $configs, ContainerBuilder $container)
{
$definition = new Definition('HelloBundle\\SomePrettyService');
$container->addDefinition($definition);
}
Помимо такого «ручного» создания кода, можно воспользоваться импортированием xml-карты сервисов, которая создается согласно определенным правилам. Очевидно, что он более удобнее и нагляднее.
# App\HelloBundle\DependencyInjection\AppHelloBundleExtension.php
public function load(array $configs, ContainerBuilder $container)
{
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.xml');
}
Однако, нам ничего не мешает использовать оба способа создания определений.
# App\HelloBundle\DependencyInjection\AppHelloBundleExtension.php
public function load(array $configs, ContainerBuilder $container)
{
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.xml');
$definition = $container->getDefinition('some.pretty.service');
// ...
// do something with $definition
// ...
}
Это удобно в том случае, когда в xml-файле задается структура определения, а в расширении (Extension) для определения подставляются значения нужных аргументов, например, из файла конфигурации. Создание собственной конфигурации немного выходит за рамки текущей статьи и может быть рассмотрена позднее. Предполагается, что в текущий момент есть коллекция с данными из конфигурации.
Теперь посмотрим, как создавать определения будущих сервисов в xml. Файл имеет следующую корневую структуру
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter>...</parameter>
...
</parameters>
<services>
<service>...</service>
...
</services>
</container>
* This source code was highlighted with Source Code Highlighter.
Каждое определение сервиса задается тегом service. Для него предусмотрены следующие атрибуты
- id — название сервиса (то, по которому этот сервис можно получать из контейнера)
- class — название класса сервиса, если он будет создаваться через конструкцию new (если сервис будет создаваться через фабрику, название класса может быть ссылкой на интерфейс или абстрактный класс)
- scope
- public — true или false — видимость сервиса
- syntetic — true или false
- abstract — true или false — является ли данное определение сервиса абстрактным, т.е. шаблоном для использования в определении других сервисов
- factory-class — название класса-фабрики для статического вызова метода
- factory-service — название существующего сервиса-фабрики для вызова публичного метода
- factory-method — название метода фабрики, к которому обращается контейнер
- alias — алиас сервиса
- parent
Атрибуты задаваемых параметров parameter
- type
- id
- key
- on-invalid
Внутри тега service могут быть вложены следующие элементы
<argument />
<tag />
<call />
* This source code was highlighted with Source Code Highlighter.
argument — передача в качестве параметра какого-либо аргумента, либо это ссылка на существующий сервис, либо коллекция аргументов.
tag — тэг, назначаемый сервису.
call — вызов метода сервиса после его инициализации. При вызове метода передаваемые параметры перечисляются с помощью вложенного тега argument.
Значения атрибутов и тегов (к примеру, названия классов) чаще всего выносят в параметры, далее используют подстановку этого параметра в атрибут или тег. Параметр всегда можно различить по наличию знака % в начале и конце. Например
<parameters>
<parameter key="some_service.class">App\HelloBundle\Service</parameter>
</parameters>
<services>
<service id="some_service" class="%some_service.class%" />
</services>
* This source code was highlighted with Source Code Highlighter.
Удобно в таком случае все параметры перечислить в одном месте, а потом использовать не один раз в определениях сервисов.
Примеры определений сервисов
Теперь более наглядно описанное выше может быть представлено на примерах:
<service id="some_service_name" class="App\HelloBundle\Service\Class">
<argument>some_text</argument>
<argument type="service" id="reference_service" /><!-- в качестве аргумента передается ссылка на существующий сервис -->
<argument type="collection">
<argument key="key">value</argument>
</argument>
<call method="setRequest">
<argument type="service" id="request" />
</call>
</service>
* This source code was highlighted with Source Code Highlighter.
Выше описанный сервис контейнером при первом обращении к нему «превращается» примерно в следующее
// инстанцированные ранее контейнером сервисы
$referenceService = ... ;
$request = ... ;
$service = new App\HelloBundle\Service\Class('some_text', $referenceService, array('key' => 'value'));
$service->setRequest($request);
Тоже самое, но в определениях Symfony2
# App\HelloBundle\DependencyInjection\AppHelloBundleExtension.php
public function load(array $configs, ContainerBuilder $container)
{
$definition = new Definition('App\HelloBundle\Service\Class');
$definition->addArgument('some_text');
$definition->addArgument(new Reference('reference_service'));
$definition->addArgument(array('key' => 'value'));
$definition->addMethodCall('setRequest', array(new Reference('request')));
$container->setDefinition('some_service_name', $definition);
}
Получить данный сервис, например, в контроллере MVC можно так
$this->container->get('some_service_name');
Думаю, что более наглядные примеры создания определений сервисов можно посмотреть в стандартных бандлах, поставляемых вместе с ядром Symfony2.
Заключение
В качестве заключения стоит отметить что Service Container в Symfony2 очень удобен, позволяет однажды сконфигурировать все необходимые для приложения сервисы и использовать их по назначению. Также стоит отметить, что в Symfony2 существует «умная» система кэширования в том числе и для определений сервисов, поэтому каждый раз добавляя или изменяя их, не забывайте чистить кэш.
Ссылки по теме
Martin Fawler: Inversion of Control Containers and the Dependency Injection pattern
Внедрение зависимости
Обращение контроля (инверсия управления)
Symfony2 — The Service Container