DI-контейнер в Yii: три способа регистрации зависимостей
Один и тот же контейнер в Yii 2 можно настроить четырьмя способами — от массива в конфиге до фабрики-замыкания. Разбираем, что выигрываешь и чем платишь за каждый.

Вы добавляете новый сервис — скажем, клиент платёжного шлюза — и решаете, как отдать его контроллерам. В Yii 2 контейнер зависимостей (yii\di\Container) принимает регистрацию минимум четырьмя способами: строкой-псевдонимом, массивом-конфигом, замыканием-фабрикой и готовым экземпляром. Все четыре рабочие, но ведут себя по-разному: одни создают объект заново на каждый запрос из контейнера, другие возвращают один и тот же экземпляр, третьи ломаются, если у зависимости есть обязательные аргументы конструктора. Ниже — когда какой способ уместен и где он вас подведёт.
Материал для Yii 2.0.x (актуальная ветка на момент написания). В Yii 1.x механизма DI в таком виде не было, там жили компоненты приложения через CApplication — к нему это не относится.
Что вообще делает контейнер в Yii
В Yii 2 есть глобальный экземпляр контейнера — Yii::$container. Через него проходит создание почти всех объектов, которые вы получаете не через new, а через Yii::createObject() или как компоненты приложения. Контроллеры, виджеты, модули — их фреймворк тоже собирает через контейнер, поэтому type-hint в конструкторе контроллера автоматически подтягивает зарегистрированную зависимость.
Регистрация делается методом set() (общий случай, объект создаётся заново при каждом обращении) или setSingleton() (объект создаётся один раз и переиспользуется). Дальше — четыре формы того, что вы передаёте вторым аргументом.
Способ 1. Псевдоним класса — строка
Самый короткий вариант: связать интерфейс с конкретной реализацией.
use yii\di\Container;
$container = new Container();
// Интерфейс -> конкретный класс
$container->set(
'app\services\PaymentGatewayInterface',
'app\services\StripeGateway'
);
// Теперь контейнер сам подставит StripeGateway,
// где в конструкторе запрошен PaymentGatewayInterface
$gateway = $container->get('app\services\PaymentGatewayInterface');Работает, только если у StripeGateway нет обязательных параметров конструктора, которые контейнер не может разрешить сам через autowiring. Если конструктор требует, например, строку API-ключа — этот способ упадёт с исключением о невозможности разрешить зависимость.
Способ 2. Массив-конфиг
Тот же псевдоним, но с параметрами. Ключ class — что создавать, остальные ключи — свойства объекта, которые проставятся после создания.
$container->set('app\services\PaymentGatewayInterface', [
'class' => 'app\services\StripeGateway',
'timeout' => 15, // публичное свойство $timeout
'currency' => 'RUB',
]);Свойства из массива проставляются после вызова конструктора — то есть они не идут в конструктор, а присваиваются созданному объекту. Это ключевое ограничение: если значение обязательно в момент конструирования, массив-конфиг не подойдёт, нужны параметры конструктора (см. ниже) или замыкание.
Способ 3. Замыкание-фабрика
Когда сборка объекта нетривиальна — зависит от других сервисов, требует логики, читает конфиг — передаёте замыкание. Оно получает сам контейнер и массив параметров.
use yii\di\Container;
use Yii;
$container->set('app\services\PaymentGatewayInterface', function (Container $c, $params, $config) {
// API-ключ обязателен в конструкторе — массив-конфиг здесь бессилен
$apiKey = Yii::$app->params['stripeKey'];
$logger = $c->get('app\services\LoggerInterface');
return new \app\services\StripeGateway($apiKey, $logger);
});Замыкание — единственный способ, где вы полностью контролируете вызов конструктора и можете достать другие сервисы из того же контейнера. Цена: сборка объекта размазана по коду-фабрике, её сложнее увидеть при чтении конфига, и такую регистрацию нельзя целиком описать в файле конфигурации приложения массивом — замыкание в конфиге работает, но хуже кешируется и хуже читается ревьюером.
Способ 4. Готовый экземпляр
Передаёте уже собранный объект. Контейнер будет отдавать именно его — фактически это singleton, зафиксированный вручную.
$gateway = new \app\services\StripeGateway($apiKey, $logger);
$container->set('app\services\PaymentGatewayInterface', $gateway);Удобно в тестах, когда нужно подсунуть замоканный объект. В продакшене — обычно нет: объект создаётся при регистрации, даже если за запрос он ни разу не понадобится.
Параметры конструктора: отдельный аргумент set()
Если у реализации есть обязательные аргументы конструктора, их можно передать третьим аргументом set() — тогда не нужно замыкание.
$container->set(
'app\services\StripeGateway',
[], // конфиг (свойства после создания)
['YOUR_API_KEY_PLACEHOLDER'] // позиционные аргументы конструктора
);Это компромисс между массивом и замыканием: аргументы идут в конструктор, но без логики. Как только для сборки нужен другой сервис или условие — переходите на замыкание.
set() против setSingleton()
Разница не косметическая и регулярно даёт баги. set() создаёт новый объект при каждом get(). setSingleton() создаёт один раз и кеширует внутри контейнера.
Если сервис хранит состояние между вызовами — счётчик, накопленный буфер, открытое соединение — и вы зарегистрировали его через set(), то разные части кода получат разные объекты и разное состояние. Ошибку заметят не сразу: на малой нагрузке всё выглядит рабочим.Для клиентов внешних API, пулов соединений, кеша-обёртки почти всегда нужен setSingleton(). Для объектов-значений и лёгких stateless-сервисов разница только в аллокациях.
Сравнение способов
| Способ | Аргументы конструктора | Логика при сборке | Где уместен |
|---|---|---|---|
| Строка-псевдоним | только autowiring | нет | интерфейс → одна реализация без обязательных параметров |
| Массив-конфиг | нет (только свойства после создания) | нет | реализация + настройка публичных свойств |
| set() + аргументы | позиционные | нет | обязательные скалярные аргументы конструктора |
| Замыкание | полный контроль | да | сборка зависит от других сервисов или условий |
| Готовый экземпляр | вы вызываете new сами | да, вне контейнера | тесты, ручной singleton |
Когда что выбирать
- Интерфейс и одна реализация без параметров — строка-псевдоним. Короче некуда, ревьюер понимает связь за секунду.
- Нужно проставить пару настроек (таймаут, флаг) — массив-конфиг.
- Конструктор требует скаляр (ключ, URL) и больше ничего — третий аргумент
set(). - Сборка тянет другие сервисы или ветвится — замыкание. Не пытайтесь впихнуть это в массив.
- Тест — готовый экземпляр или замыкание, возвращающее мок.
Где это ломается
- Циклическая зависимость. A требует B в конструкторе, B требует A — контейнер уйдёт в бесконечную рекурсию и упадёт. Лечится сеттер-инъекцией одной из сторон или разрывом цикла через ленивую фабрику.
- Autowiring по имени параметра, а не по типу. Если у аргумента конструктора нет type-hint или тип скалярный, контейнер его не разрешит — получите исключение о недостающем параметре. Массив-конфиг тут не спасёт, свойства ставятся после конструктора.
- set() вместо setSingleton() для stateful-сервиса — разошедшееся состояние, описанное выше.
- Замыкание в конфиге приложения. Работает, но конфиг с замыканиями хуже поддаётся кешированию и статическому анализу. На больших конфигах это заметно.
Что делать дальше
Проверьте по проекту, какие сервисы зарегистрированы через set(), но хранят состояние — переведите их на setSingleton(). Затем посмотрите файл конфигурации приложения (секция container в конфиге компонентов): регистрацию, дублирующуюся между веб- и консольным входом, вынесите в общий файл. Autowiring и полный список форм записи описаны в официальном руководстве Yii по контейнеру и DI — сверяйтесь с ним для вашей минорной версии, поведение autowiring уточнялось между релизами 2.0.x.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Yii 2 Guide — Dependency Injection Container
Частые вопросы
А если у меня Yii 1.x — эти способы подойдут?
Что делать, если контейнер бросает исключение о невозможности разрешить параметр?
Зарегистрировал сервис, но получаю каждый раз новый объект — почему?
Можно ли класть замыкания прямо в конфиг приложения?
Как подменить сервис на мок в тестах?
Контейнер зациклился и упал — что делать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.