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

Приложение на Yii 2 стало отдавать чужие данные: пользователь A видел корзину пользователя B. Причина — сервис зарегистрировали как синглтон и сложили в него ссылку на текущего пользователя. В веб-режиме FPM это не всплывало, а под RoadRunner, где процесс живёт между запросами, состояние потекло. Ниже — набор ошибок при работе с контейнером зависимостей Yii 2, которые дорого стоят в продакшене, и как их ловить на этапе кода.
Статья про Yii 2 (ветка 2.0.x, актуальная на момент написания). Работа контейнера в Yii 3 отличается — там отдельный пакет yiisoft/di с иной семантикой, здесь его не касаемся.
Как контейнер в Yii 2 вообще работает
В Yii 2 есть yii\di\Container, доступный через Yii::$container, и есть yii\di\ServiceLocator — то, чем на самом деле является Yii::$app. Это два разных механизма, и путаница между ними — источник половины проблем.
- Container — создаёт объекты, разрешая зависимости конструктора автоматически (autowiring) через рефлексию.
- ServiceLocator — реестр уже настроенных компонентов приложения (
Yii::$app->db,Yii::$app->cache). Компоненты приложения по умолчанию — синглтоны в пределах локатора.
Yii::createObject() внутри обращается к контейнеру. Именно через него стоит создавать сервисы, чтобы получить внедрение зависимостей, а не через new.
Термин: autowiring
Autowiring — автоматическое разрешение зависимостей: контейнер смотрит на типы параметров конструктора через рефлексию и подставляет подходящие объекты сам, без явного описания каждой связи.
Ошибка 1. Синглтон с состоянием запроса
Самая дорогая ошибка. Вы регистрируете сервис через setSingleton() и складываете в его свойство что-то, привязанное к текущему запросу: пользователя, локаль, корзину.
// Опасно: синглтон хранит текущего пользователя
Yii::$container->setSingleton(CartService::class);
class CartService
{
private $userId; // переживёт запрос в долгоживущем процессе
public function setUser(int $userId): void
{
$this->userId = $userId;
}
}
В классическом PHP-FPM процесс умирает после запроса, и проблема прячется. Под RoadRunner, Swoole или в консольных воркерах процесс живёт долго, синглтон не пересоздаётся — и следующий запрос получает чужое состояние.
Синглтон в контейнере — это обещание «этот объект один на процесс». Всё, что зависит от конкретного запроса, в такой объект класть нельзя. Передавайте контекст аргументом метода, а не свойством.
Правильно — держать в синглтоне только то, что действительно не меняется между запросами (клиент к внешнему API, конфиг), а данные запроса прокидывать параметром:
public function getItems(int $userId): array
{
// userId приходит на каждый вызов, не хранится в объекте
}
Ошибка 2. Считать, что set() возвращает один и тот же объект
Разница между set() и setSingleton() путает регулярно.
set()— контейнер создаёт новый объект при каждомget().setSingleton()— объект создаётся один раз, дальше возвращается тот же экземпляр.
Если вы регистрируете через set() дорогой в создании сервис (подключение, пул) и ожидаете переиспользования — вы платите созданием на каждый запрос к контейнеру. И наоборот: если ждёте свежий объект, а зарегистрировали синглтон, получаете накопленное состояние.
Ошибка 3. Циклические зависимости
Сервис A требует в конструкторе B, а B требует A. Контейнер при autowiring уходит в бесконечную рекурсию и падает с исчерпанием памяти или переполнением стека — понятного сообщения об этом вы не получите.
class Orders
{
public function __construct(Invoices $invoices) {}
}
class Invoices
{
public function __construct(Orders $orders) {} // цикл
}
Цикл — почти всегда сигнал неправильного разбиения ответственности. Варианты выхода:
- Выделить третий класс, от которого зависят оба, и убрать взаимную ссылку.
- Передавать зависимость не в конструктор, а методом, когда она реально нужна (setter-инъекция).
- Инъектировать не сам сервис, а фабрику/замыкание, создающее его лениво.
Ошибка 4. new вместо контейнера
Внутри сервиса вы пишете new PaymentGateway($config) — и все зависимости PaymentGateway теперь ваша забота вручную. Автопривязка не работает, замена реализации в тестах невозможна, конфигурация дублируется.
Создавайте объекты через контейнер:
// autowiring разрешит зависимости конструктора
$gateway = Yii::createObject(PaymentGateway::class);
// с параметрами конструктора по позициям
$gateway = Yii::createObject(PaymentGateway::class, [$config]);
Для тестов это разница между «подменил определение в контейнере одной строкой» и «переписал полкласса».
Ошибка 5. Логика в определении контейнера
В config/main.php в секции container.definitions закладывают замыкание с обращением к базе, чтением файла или сетевым вызовом. Это выполнится при первом создании объекта — иногда в неожиданный момент, иногда в консольной команде, где нужного компонента нет.
Определение контейнера должно только собирать объект и передавать зависимости. Всё, что делает работу, — внутри методов сервиса, а не в его сборке.
Ошибка 6. Привязка интерфейса без учёта области
Классический приём — привязать интерфейс к реализации:
Yii::$container->set(
LoggerInterface::class,
FileLogger::class
);
Проблема начинается, когда одному месту нужен файловый логгер, а другому — отправка в очередь. Глобальная привязка одна на всё приложение. Здесь помогает третий аргумент set() с параметрами, либо явное определение для конкретного класса-потребителя через параметр конструктора в его определении. Точный синтаксис для связывания по контексту сверяйте в разделе о контейнере в Definitive Guide — детали различаются между минорными версиями 2.0.x.
Как отличить одну ошибку от другой
| Симптом | Вероятная причина | Что проверить |
|---|---|---|
| Данные одного пользователя видит другой | Состояние запроса в синглтоне | Свойства объектов, зарегистрированных через setSingleton() |
| Fatal error: allowed memory exhausted при старте | Циклическая зависимость | Конструкторы взаимно ссылающихся классов |
| Тесты не изолируются, влияют друг на друга | Синглтон не сбрасывается между тестами | Clear контейнера в setUp/tearDown |
| Замена реализации не подхватывается | Объект создан через new | Поиск new по кодовой базе сервисов |
| Ошибка в консольной команде, но не в вебе | Логика в определении контейнера или отсутствующий компонент | Замыкания в container.definitions |
Что ломается в тестах
Если сервисы — синглтоны, между тестами они не пересоздаются, и мок из первого теста тянется во второй. Контейнер приложения в тестах стоит сбрасывать явно. Yii при создании приложения в тестовом окружении пересоздаёт Yii::$app, но Yii::$container живёт дольше — про это забывают. Регистрируйте подмены в setUp() и снимайте их в tearDown(), не полагаясь на автоматический сброс.
Что делать дальше
Пройдитесь по кодовой базе с тремя проверками: поиск new внутри классов сервисов, ревизия всех setSingleton() на предмет изменяемого состояния, и проверка конструкторов на взаимные ссылки. Если планируете переезд на долгоживущий рантайм (RoadRunner, Swoole) — этот аудит обязателен до переезда, потому что там всплывёт всё, что PHP-FPM прощал. Показатель, за которым стоит следить в проде после изменений в контейнере, — рост потребления памяти воркером от запроса к запросу: он выдаёт накопление состояния в синглтонах.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: Yii 2.0 Definitive Guide — Dependency Injection Container, Yii 2.0 Definitive Guide — Service Locator
Частые вопросы
А если у меня Yii 3, а не Yii 2?
Как понять, что проблема именно в синглтоне, а не в чём-то ещё?
В чём разница между Yii::$container и Yii::$app для внедрения зависимостей?
Использовать set() или setSingleton()?
Что делать с циклической зависимостью, если рефакторинг невозможен прямо сейчас?
Почему нельзя просто создавать сервисы через new?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.