Начал разбираться с 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, более подробно о нем читайте в официальной документации.