Pull to refresh

Symfony2 Dependency Injection в разрезе

Reading time7 min
Views33K
Из статьи можно узнать как стартует и работает приложение 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
Tags:
Hubs:
Total votes 28: ↑27 and ↓1+26
Comments14

Articles