<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · JUNIOR ЧАСТО ПИТАЮТЬ

Що таке впровадження залежностей і навіщо контейнер?

Впровадження залежностей означає, що клас отримує готові співпрацівники ззовні (найчастіше через конструктор), а не створює їх сам. Контейнер це фабрика з реєстром, яка вміє зібрати граф обʼєктів за типами параметрів конструктора і підставити реалізацію там, де оголошено інтерфейс.

Клас усередині методу робить `new PDO(...)`. Що тут не так і як переписати?
Чим впровадження залежностей відрізняється від контейнера?
Як контейнер здогадується, що передати в конструктор, якщо я нічого не реєстрував?
Ви інжектите інтерфейс, у якого одна реалізація і ніколи не буде другої. Навіщо?
DI контейнер autowiring PSR-11 інтерфейси тестування

Впровадження залежностей вкладається в одне речення: клас не добуває собі співпрацівників, а отримує їх готовими. Порівняйте два варіанти. У першому RegisterUser усередині методу пише new SmtpMailer(getenv('MAIL_DSN')) і намертво прибиває до себе конкретний транспорт, змінну оточення й момент створення підключення. У другому той самий клас оголошує в конструкторі параметр типу Mailer, і питання «який саме мейлер» вирішується на рівень вище. Найшвидше виграш видно в тестах: щоб перевірити реєстрацію користувача, не треба піднімати SMTP чи мокати статичні виклики, достатньо передати простий клас-заглушку, який складає листи в масив.

Конструктор і сеттер вирішують одну задачу з різною жорсткістю. Параметр конструктора робить залежність частиною контракту: без неї обʼєкт створити неможливо, поле можна оголосити private readonly, а список параметрів чесно показує, скільки всього класу треба для роботи. Сеттер лишає вікно, у якому обʼєкт уже існує, але ще неповний, і кожен метод змушений рахуватися з null. Звідси й вузька ніша сеттера: справді необовʼязкові речі на кшталт логера з NullLogger за замовчуванням (LoggerAwareInterface з PSR-3), успадковані класи, створення яких ви не контролюєте, і розрив циклу як тимчасовий пластир. Побічний ефект конструкторної інʼєкції корисний сам по собі: коли параметрів набирається сім, це вже не проблема стилю, а сигнал, що клас узяв на себе кілька відповідальностей.

Інверсія залежностей додає до цього питання напряму. Мало передати SmtpMailer ззовні, якщо тип параметра теж SmtpMailer: прикладний код і далі залежить від деталі інфраструктури. Тому в сигнатурі стоїть інтерфейс Mailer, і власником цього інтерфейсу є споживач, а не бібліотека транспорту. У модульній структурі це буквально видно по теках: інтерфейс лежить у Domain або Application, реалізація з залежністю від Symfony Mailer чи cURL - в Infrastructure, і стрілка залежності вказує всередину. Зворотний бік такий самий важливий: інтерфейс не потрібен там, де немає ні виходу назовні, ні другої реалізації. Калькулятор знижок, парсер рядка, Money з арифметикою чудово живуть як фінальні класи, а MoneyInterface з єдиним нащадком лише додає файл, який доведеться відкривати на кожному переході «до реалізації».

Контейнер зʼявляється тоді, коли ручне складання графа перестає вміщатися в голову. По суті це реєстр фабрик плюс автозвʼязування: коли для запитаного ідентифікатора немає зареєстрованої фабрики, контейнер бере ReflectionClass, дивиться типи параметрів конструктора і рекурсивно будує кожен із них, як у прикладі коду. Далі починаються відмінності реалізацій. Laravel і PHP-DI роблять рефлексію в рантаймі (PHP-DI вміє кешувати результат через enableCompilation()), Symfony компілює конфігурацію в згенерований PHP-клас під час білду, тому в проді жодної рефлексії вже немає, а помилка привʼязки випливає при компіляції контейнера, а не на живому запиті. Спільна для всіх межа: автозвʼязування виводить тільки те, що є в типах. Інтерфейс сам себе створити не може, string $dsn не має де взяти значення, і обидва випадки закриваються явною фабрикою. Стандарт PSR-11 описує лише два методи, get() і has(), для тих, хто обʼєкти споживає; спосіб реєстрації кожен контейнер визначає сам, тому переносити конфігурацію між ними доводиться руками.

Місце контейнера - композиційний корінь: bootstrap, service provider, DI-конфіг. Усередині класів застосунку його не має бути видно, інакше DI непомітно вироджується в service locator з усіма наслідками, описаними у follow-up. Так само не варто тягнути через контейнер речі, які нічого не приховують: DTO, сутності, value objects створюються звичайним new, а маленький скрипт чи одноразова консольна команда живе без контейнера взагалі. Окремий випадок для суперечки - new DateTimeImmutable() усередині методу: сам по собі він нешкідливий, поки тест не почне залежати від поточного часу; тоді джерело часу виноситься в залежність, і в PHP для цього вже є стандартний Psr\Clock\ClockInterface (PSR-20). Правило, яке добре працює на практиці: інжектимо те, що ходить назовні або має змінюватися в тестах, решту створюємо на місці.

declare(strict_types=1);

interface Mailer
{
    public function send(string $to, string $text): void;
}

final class SmtpMailer implements Mailer
{
    public function __construct(private string $dsn) {}  // скаляр контейнер не вгадає

    public function send(string $to, string $text): void { /* SMTP */ }
}

final class RegisterUser
{
    // залежність у конструкторі: обʼєкт або зібраний повністю, або не існує
    public function __construct(private Mailer $mailer) {}

    public function __invoke(string $email): void
    {
        $this->mailer->send($email, 'Вітаємо на борту');
    }
}

final class Container
{
    /** @var array<string, callable(self): object> */
    private array $factories = [];

    /** @var array<string, object> */
    private array $shared = [];

    public function set(string $id, callable $factory): void
    {
        $this->factories[$id] = $factory;
    }

    public function get(string $id): object
    {
        return $this->shared[$id] ??= isset($this->factories[$id])
            ? ($this->factories[$id])($this)
            : $this->autowire($id);
    }

    /** Автозвʼязування: читаємо типи конструктора і рекурсивно будуємо аргументи */
    private function autowire(string $id): object
    {
        $args = array_map(
            fn (ReflectionParameter $p) => $this->get((string) $p->getType()),
            (new ReflectionClass($id))->getConstructor()?->getParameters() ?? [],
        );

        return new $id(...$args);
    }
}

$container = new Container();
$container->set(Mailer::class, static fn () => new SmtpMailer(getenv('MAIL_DSN')));

// RegisterUser ніде не зареєстрований, контейнер зібрав його сам за типом Mailer
($container->get(RegisterUser::class))('dev@phpukraine.com');
Що DI це передача готового обʼєкта ззовні, а контейнер лише інструмент, який автоматизує складання; DI чудово працює й без контейнера, у ручному bootstrap-файлі.
Чому конструктор кращий за сеттер: обʼєкт не існує в напівзібраному стані, залежності видно в сигнатурі, поля можна зробити `private readonly`.
Що інверсія залежностей це про напрям: конкретний клас залежить від інтерфейсу, який оголошено поруч зі споживачем, а не в шарі інфраструктури.
Що автозвʼязування будує тільки те, що можна вивести з типів: інтерфейс і скаляр (DSN, ключ API) контейнер сам не розвʼяже, для них потрібна привʼязка або фабрика.
Що надто довгий список параметрів конструктора це сигнал розділити клас, а не привід перейти на сеттери.
Вважати, що DI це «коли підключив контейнер»: клас із `new` усередині лишається нетестованим навіть у застосунку з найкрутішим контейнером.
Замість інʼєкції тягнути залежності з контейнера всередині методу (`$container->get(Mailer::class)`): це service locator, залежності зникають із сигнатури, а тест доводиться піднімати разом з усім застосунком.
Створювати інтерфейс на кожен клас «щоб було по SOLID», зокрема на класи без зовнішніх ефектів, і отримати подвійну кількість файлів без жодної підміни.
Інжектити залежності у value objects і сутності: `Money`, `Email`, `OrderId` створюються через `new`, і контейнер про них знати не повинен.
Робити властивість `nullable` з сеттером, а потім у кожному методі писати `if ($this->logger !== null)` замість того, щоб передати в конструктор `NullLogger`.
ПОРАДА

Скажіть коротку формулу: «залежність приходить у конструктор, тип параметра це інтерфейс там, де є вихід назовні, а контейнер лише складає граф і не має зʼявлятися в коді класу». Потім наведіть свій приклад тесту, де реальний клієнт API замінили на фейк без жодного mock-фреймворка: це переконує краще за визначення.

оновлено 9 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Автозвʼязування виводить залежності з type-hint параметрів, а не з їхніх імен, і рекурсивно створює конкретні класи без реєстрації. Інтерфейс інстанціювати не можна, тому для нього потрібна привʼязка або фабрика; те саме стосується скалярних параметрів.