Dependency Injection

Начал разбираться с DI-контейнерами в симфони2. На описании принципа внедрения зависимостей останавливаться не буду, об этом можно много где прочитать.

Предположим у нас есть такие классы:

<?php

class Mailer {  
    public function send($email, $msg) {
        // ...
    }
}

class Log {  
    public function __construct($filename) {
        // ...
    }

    public function info($text) {
        // ...
    }
}

Класс Mailer умеет отправлять письма на указанный адрес, класс Log умеет писать сообщения в файл, путь до которого передается ему в конструкторе. Теперь предположим что при отправке сообщения нужно этот факт залогировать. Как это сделать? Очень часто в таких случая класс лога делают синглетоном и используют как-то так:

Log::getInstance()->info();  

Но синглетоны не наш метод, так как это те же глобальные переменные, только в профиль. К тому же если такой класс будет синглетоном, то для того чтобы писать сообщения в еще один лог, нам нужно будет создать еще один класс. Используем DependencyInjection из Symfony2, к счастью это отдельный компонент и его легко использовать практически в любом приложении. Подробности о его установке в кастомном проекте можно почитать здесь.

Объявим сервис лог

services:  
  log:
    class: Log
    arguments: ['tmp/application.log']

Теперь если у нас есть инстанс контейнера, то мы можем получить объект лога так:

$container->get('log');

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

Как теперь получить объект лога в методе send? Есть несколько способов. Первый - сделать контейнер глобальной переменной (так например сделано в Yii: Yii::app()->log), второй - передавать инстанс контейнера в конструктор класса, такой подход распространен в Symfony2 и ZF2. Но мы сейчас поступим по другому.

Немного изменим класс Mailer:

<?php

class Mailer {  
    protected $log;

    public function __construct(Log $log) {
        $this->log = $log;
    }

    public function send($email, $msg) {
        // ...
        $this->log->info('...');
    }
}

И зарегистрируем класс Mailer как сервис:

services:  
  log:
    #...
  mailer:
    class: Mailer
    arguments: ['@log']

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

Этим возможности DI в симфони не ограничиваются, кратко перечислю их. Кроме передачи параметров в конструктор он умеет вызывать методы и сетать свойства класса.

Можно указать фабрику с помощью которой контейнер будет создавать инстансы класса.

Контейнер умеет лениво создавать сервисы. Т.е. когда ты запрашиваешь сервис у контейнера, он тебе возвращает прокси объект, а настоящий объект создаст только в тот момент когда произойдет первый вызов метода этого сервиса.

Если у вас много сервисов с похожей конфигурацией, то можно не копипастить конфиг, а сделать один общий и от него наследовать конфигурации каждого сервиса.

Можно испльзовать параметры в конфиге:

parameters:  
  logs: 'tmp/logs'
services:  
  log:
    class: Log
    arguments: ['%logs%/application.log']
  customLog:
    class: Log
    arguments: ['%logs%/custom.log']

А что делать если нужен не один инстанс класса каждый раз, а разный? Т.е. по сути нужно всего лишь конфигурировать все инстансы через общий конфиг. Для этого нужно использовать scope. По дефолту он равен "container", если же его установить в "prototype", то каждый раз когда вы будет делать get для этого сервиса вы будете получать новый инстанс.

services:  
  mailer:
    class: Mailer
    arguments: ['@log']
    scope: prototype

Это безусловно не все что умеет DependencyInjection в Symfony, более подробно о нем читайте в официальной документации.