Пользователь
0,0
рейтинг
15 апреля 2011 в 17:43

Разработка → 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
Попков Алексей @patashnik
карма
11,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (14)

  • +4
    Думаю, многим будет полезно.
    К сожалению, пропущено описание interface injection, интересная возможность о которой мало документации.
    Причем тут SOA, если честно, не понял.

    ЗЫ ну и с подсветкой кода какие-то проблемы.
    • +1
      Соглашусь, что SOA тут не причем и автор наверное не знает сути этого понятия. Под Service в SOA в частном случае очень часто понимают какой-либо веб-сервис, общение с которым происходит при помощи известных протоколов вроде SOAP.
      • 0
        Согласен, понимаю, что SOA к DI отношения не имеет, но я хотел связать между собой эти понятия в архитектуре Symfony2, видимо, не получилось.
        • 0
          Ну тема тоже дельная, но если хотите об этом рассказать, то лучше — отдельным топиком. IoC — сама по себе более чем самодостаточная тема.
        • 0
          Тут вопрос далеко не простой. Правильно спроектированные симфонийские сервисы могут отвечать всем принципам SOA, кроме платформы. И всегда остается возможность обернуть вызов стороннего сервиса в симфонийский сервис, как и предоставить гейт для вызова симфонийского сервиса извне. В то же время в контейнер можно включить сервис, который принципам SOA вообще не соответствует.
  • 0
    Прочитал спецом, всегда было интересно — а как вообще в PHP используют IoC, и, в особенности, DI.

    И не увидел, собственно, DI. Прямое обращение к контейнеру «за нужным объектом» — это паттерн Service Locator, а не Dependency Injection. Точнее, вы конечно «внедряете» контейнер через конструктор — но это не показательно. Обычно через конструктор внедряется что-нибудь другое, конкретный сервис, и это и будет DI (чтобы вся цепочка объектов ничего не знала о контейнере — собственно, главный минус Service Locator'а как раз в том, что получается зависимость от Locator'а).

    P.S. И да, как уже сказали выше, SOA тут ни при чем. Это совсем другая тема.
    • 0
      Сервис 'some_service_name' в качестве аргумента конструктора принимает другой сервис, назовем его к примеру 'reference_service'. Контейнер знает про все эти зависимости, и в случае обращения к первому сервису создает сам экземпляр второго и подставляет в первый.
      • 0
        А, все, сорри, упустил этот момент. Значит все-таки «правильный» DI-ный auto-wiring есть. Просто сразу не заметил, плюс смутили примеры про Симфони в интернете, где сервисы запрашиваются у контейнера прямо внутри action-ов контроллеров.
        • 0
          Вообще это рассматривается как плохая практика инжектить контейнер в контроллер. Поэтому рекомендуют контроллеры сделать сервисами тоже и инжектить сервисы через сеттеры и конструктор.
          • 0
            Я то это понимаю прекрасно :) Я бы еще про XML для регистраций забыл бы как страшный сон.
  • 0
    В конечном итоге да, получение сервиса внутри action-контроллера выглядит именно как обращение к Service Locator'у.
    • 0
      И чем это лучше тупого registry?
  • +2
    Что-то как-то over-engineered. Древний phemto куда элегантнее.
  • 0
    Чем это отличается (не реализация, а именно смысл) от паттерна «реестр»? Ну кроме того, что здесь я так понимаю, мы можем описать интерфейс самого сервиса, но зачем его таким способом описывать, чем стандартные интерфейсы хуже?

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