Внедрение зависимостей и DI-контейнер
Вопрос, на котором проверяют, понимаете ли вы, почему фреймворки устроены именно так.
Dependency Injection — приём, при котором объект не создаёт свои зависимости сам, а получает их извне (чаще всего через конструктор). DI-контейнер — служебный объект, который знает, как эти зависимости собрать.
Вопрос 1: «Что такое внедрение зависимостей и зачем оно нужно?»
Что проверяет интервьюер
Умеете ли вы объяснить пользу без заклинаний вроде «слабая связанность». Начните с проблемы:
// плохо: класс жёстко привязан к конкретной реализации
class OrderService {
private FileLogger $logger;
public function __construct() {
$this->logger = new FileLogger('/var/log/app.log');
}
}
Такой сервис невозможно протестировать без записи в файл, невозможно переключить на другой логгер и невозможно переиспользовать в CLI-команде. Внедрение зависимости решает всё сразу:
<?php
interface LoggerInterface { public function log(string $m): void; }
class EchoLogger implements LoggerInterface {
public function log(string $m): void { echo "[log] $m\n"; }
}
class OrderService {
public function __construct(private LoggerInterface $logger) {}
public function place(string $item): void {
$this->logger->log("заказ: $item");
}
}
class Container {
private array $factories = [];
private array $instances = [];
public function set(string $id, callable $factory): void { $this->factories[$id] = $factory; }
public function get(string $id): object {
return $this->instances[$id] ??= ($this->factories[$id])($this);
}
}
$c = new Container();
$c->set(LoggerInterface::class, fn() => new EchoLogger());
$c->set(OrderService::class, fn(Container $c) => new OrderService($c->get(LoggerInterface::class)));
$c->get(OrderService::class)->place('кофе');
var_dump($c->get(OrderService::class) === $c->get(OrderService::class));
Вывод:
[log] заказ: кофе bool(true)
Здесь видно два ключевых свойства контейнера: он умеет собирать граф зависимостей и по умолчанию хранит собранные сервисы как разделяемые (второй get() вернул тот же экземпляр — оператор ??= кэширует). В тесте вместо EchoLogger подставляется заглушка — и OrderService об этом даже не узнает.
Вопрос 2: «Чем DI отличается от контейнера и от service locator?»
Здесь чаще всего путаются, а разница простая:
- DI — это принцип: зависимости приходят снаружи. Его можно применять вообще без библиотек, просто передавая объекты в конструктор.
- DI-контейнер — инструмент, который автоматизирует сборку. В Symfony он строит объекты по конфигурации и кэширует скомпилированный контейнер в PHP-файл; в Laravel — по «биндингам» и рефлексии.
- Service locator — антипаттерн, при котором класс сам лезет в контейнер:
$logger = $container->get('logger');. Формально зависимость тоже приходит извне, но по сигнатуре конструктора её больше не видно, и класс оказывается привязан к контейнеру.
Отсюда правило: контейнер должен «жить» на границе приложения (в ядре, в фабриках контроллеров), а бизнес-логика — принимать конкретные интерфейсы в конструкторе. Стандарт PSR-11 описывает минимальный интерфейс контейнера — get() и has(), — чтобы библиотеки могли работать с любым контейнером.
Вопрос 3: «Что такое autowiring?»
Автоматическое разрешение зависимостей: контейнер читает через рефлексию типы параметров конструктора и сам создаёт нужные объекты. Именно поэтому в Symfony и Laravel достаточно указать тип-подсказку интерфейса — конфигурацию писать не нужно.
// упрощённый autowiring через Reflection
public function make(string $class): object {
$ctor = (new ReflectionClass($class))->getConstructor();
if ($ctor === null) {
return new $class();
}
$args = [];
foreach ($ctor->getParameters() as $param) {
$type = $param->getType();
$args[] = $this->get($type->getName()); // рекурсивно собираем зависимость
}
return new $class(...$args);
}
Полезные уточнения для сильного ответа: скалярные параметры (строки, числа) autowiring разрешить не может — их задают в конфигурации; рефлексия небесплатна, поэтому боевые контейнеры компилируют результат в кэш; а циклическая зависимость двух сервисов приводит к бесконечной рекурсии и обычно ловится самим контейнером с понятной ошибкой.
Типичные ошибки кандидатов
- Ставят знак равенства между DI и контейнером: «DI — это когда используешь Symfony DI».
- Внедряют контейнер в сервисы и называют это внедрением зависимостей — это service locator.
- Внедряют реализации вместо интерфейсов, теряя главный смысл приёма.
- Забывают, что зависимости бывают не только через конструктор: есть setter- и method-injection (последний широко применяется в контроллерах Symfony).
- Не знают про PSR-11 и про то, что контейнер по умолчанию отдаёт один и тот же экземпляр сервиса.
Как ответить кратко
«DI — принцип: класс не создаёт зависимости сам, а принимает их в конструкторе, причём по интерфейсу. Это даёт подменяемость и тестируемость. Контейнер — инструмент, который собирает граф объектов, кэширует их и умеет autowiring через рефлексию; его интерфейс описан в PSR-11. Если класс сам обращается к контейнеру за зависимостями — это service locator, антипаттерн: зависимости перестают быть видны в сигнатуре».