Внедрение зависимостей и 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, антипаттерн: зависимости перестают быть видны в сигнатуре».

Проверьте себя
1. Класс внутри своего метода вызывает $container->get('logger'). Как это называется?
AConstructor injection
BAutowiring
CService locator — антипаттерн, скрывающий зависимости
DИнверсия управления в чистом виде
2. Что такое autowiring в DI-контейнере?
AАвтоматическая перезагрузка конфигурации при изменении файлов
BАвтоматическое создание интерфейсов для классов
CКэширование результатов методов сервиса
DОпределение зависимостей по типам параметров конструктора через рефлексию
3. Какой PSR описывает интерфейс контейнера внедрения зависимостей?
APSR-11
BPSR-4
CPSR-7
DPSR-3