<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
YII · MIDDLE

Як влаштований DI-контейнер і конфігурація застосунку в Yii2?

У Yii2 два різні реєстри: `Yii::$container` (DI-контейнер, резолвить конструктори через рефлексію і за замовчуванням віддає новий обʼєкт) і ServiceLocator застосунку з секції `components` (ліниві синглтони на запит, доступні як `Yii::$app->db`). Обидва створюють обʼєкти через `Yii::createObject()`, тому конфіг у Yii2 це масив із `class` плюс публічні властивості, а не набір closure-привʼязок, як у Laravel.

Чому `Yii::$container->set()` віддає новий обʼєкт на кожен виклик, а `Yii::$app->db` завжди той самий?
Куди писати `class`: у `components` чи в `container.definitions`?
Треба зменшити кількість кнопок у всіх пейджерах проєкту, не правлячи vendor. Як?
У тесті потрібен фейковий mailer. Що саме ви перевизначаєте і чому цього не видно в контейнері?
Yii2 DI Container ServiceLocator конфігурація

Усе створення обʼєктів у Yii2 проходить через одну точку: Yii::createObject(). Це тонка обгортка над Yii::$container->get(), а сам Yii::$container це екземпляр yii\di\Container, який створюється в Yii.php ще до застосунку. Приймає він три форми: імʼя класу, масив із ключем class і рештою публічних властивостей, або callable. Коли визначення для класу немає, контейнер просто резолвить конструктор: getDependencies() читає типи параметрів через рефлексію, підставляє замість класових типів Instance::of() і рекурсивно їх розгортає, а метадані рефлексії кешує, щоб не читати їх удруге. Скалярні параметри й ті, що мають default, контейнер не чіпає. Якщо клас реалізує yii\base\Configurable (тобто будь-який спадкоємець BaseObject), масив конфігу передається останнім аргументом конструктора і застосовується до init(); для звичайних класів властивості присвоюються після створення.

Другий реєстр, який плутають із контейнером, це сам застосунок. yii\web\Application наслідує Module, а той yii\di\ServiceLocator, тому секція components у конфігу це список визначень локатора. Звернення Yii::$app->db іде через __get() у ServiceLocator::get('db'), і ось там обʼєкт створюється при першому запиті (тим самим Yii::createObject()), кладеться в _components і далі повертається без перестворення. Компонент, отже, синглтон у межах запиту, до того ж ледачий: описані в конфігу, але жодного разу не викликані компоненти не займають ні памʼяті, ні звʼязків із БД. Винятки перелічені в bootstrap: ці id ініціалізуються одразу на старті. Ваші components мерджаться з coreComponents(), тому змінити одну властивість urlManager можна, не переписуючи його цілком. Зворотний бік розділення реєстрів: Yii::$container нічого не знає про db, і щоб класу дістався саме компонент застосунку, у конфігу ставлять Instance::of('db') або приймають властивість типу public $db = 'db' і розгортають її в init() через Instance::ensure($this->db, Connection::class), як роблять yii\rbac\DbManager і yii\caching\DbCache.

Різниця між definitions і singletons обходиться дорого саме тому, що на вигляд секції однакові. Yii::$container->set() (і секція definitions у конфігу, доступна з 2.0.11) означає «ось рецепт», і кожен get() виконує рецепт заново. Сервіс із внутрішнім кешем, пул зʼєднань або будь-що, що має бути одне на процес, реєструють через setSingleton() чи секцію singletons. Орієнтир при виборі: якщо два різні обʼєкти одного класу в одному запиті ламають логіку, потрібен синглтон; якщо обʼєкт має стан на одну операцію (білдер звіту, рендерер конкретного документа), новий екземпляр безпечніший. Найкорисніший побічний ефект того, що фреймворк усюди кличе createObject(), це глобальна підміна: Yii::$container->set(LinkPager::class, ['maxButtonCount' => 5]) змінює всі пейджери проєкту, set(ActiveForm::class, [...]) усі форми, а set(SomeVendorClass::class, MyClass::class) дозволяє замінити клас із vendor без копіювання коду. Працює це рівно доти, доки в коді не зʼявляється new.

Конфігурація в Yii2 це звичайні PHP-масиви. Basic-шаблон тримає config/web.php, console.php, db.php, params.php, і вхідний скрипт робить new yii\web\Application($config). Advanced-шаблон додає шари common/frontend/backend і зливає їх ArrayHelper::merge(), а файли *-local.php лежать у .gitignore і генеруються php init із теки environments. Команди config:cache тут немає й вона не потрібна, бо масиви й так кешує opcache, але звідси правило: у конфігу не місце обчисленням, які виконаються на кожен запит. .env теж не з коробки, тому секрети або в *-local.php, або через getenv(). Плюс специфіка Yii: аліаси (@app, @runtime, @webroot, свої) задаються в тому ж конфігу, а Yii::getAlias() використовує половина фреймворку.

З Laravel Yii2 розходиться в трьох місцях. У Laravel один контейнер на все, включно з тим, що в Yii2 живе в components; тут реєстрів два, і Yii::$app->set() та Yii::$container->set() не взаємозамінні. Різний і спосіб опису: Laravel описує створення closure у сервіс-провайдері з розділенням register/boot, Yii2 описує результат масивом публічних властивостей, а роль boot грає bootstrap і init() компонента. Найсильніше відрізняється обсяг автоматики. Laravel резолвить інтерфейси контекстно (when()->needs()->give()), тегує привʼязки, впорскує залежності в методи контролерів, jobs і closure; Yii2 має одне визначення на клас глобально, без контекстної привʼязки, зате з 2.0.36 вміє впорскувати в параметри екшена через bindInjectedParams() і в довільний callable через Container::invoke(). Статичний доступ є в обох (Yii::$app->mailer проти фасадів), і в обох він однаково заважає тестам, поки залежність не приходить через конструктор.

// config/web.php: components і container це два різні реєстри
use yii\db\Connection;
use app\service\{Geocoder, MailerInterface, PdfRenderer, SmtpMailer};

return [
    'id' => 'app',
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log'],                  // створюється одразу, не ліниво
    'components' => [                        // ServiceLocator: один обʼєкт на запит
        'db' => [
            'class' => Connection::class,    // 'class' + публічні властивості
            'dsn' => 'pgsql:host=127.0.0.1;dbname=app',
            'username' => 'app',
            'password' => getenv('DB_PASSWORD'),   // секрет не в git
            'enableSchemaCache' => true,
        ],
    ],
    'container' => [                         // з 2.0.11 іде в Yii::$container
        'definitions' => [
            // новий обʼєкт на кожен createObject()
            PdfRenderer::class => ['class' => PdfRenderer::class, 'dpi' => 150],
            // глобальна підміна налаштувань класу фреймворку
            yii\widgets\LinkPager::class => ['maxButtonCount' => 5],
        ],
        'singletons' => [
            MailerInterface::class => SmtpMailer::class,
            // callable отримує ($container, $params, $config)
            Geocoder::class => fn ($c) => new Geocoder(getenv('GEO_KEY')),
        ],
    ],
];

// --- будь-де в коді ---
$a = Yii::createObject(PdfRenderer::class);   // те саме, що Yii::$container->get()
$b = Yii::createObject(PdfRenderer::class);   // $a !== $b: definitions не синглтон
$m = Yii::$container->get(MailerInterface::class);  // той самий до кінця запиту
Yii::$app->db === Yii::$app->get('db');       // компонент: локатор, не контейнер
Що `Yii::createObject()` це фасад над `Yii::$container->get()`, а не `new`: саме тому визначення з `container.definitions` впливають на віджети, AR і контролери, які створює фреймворк.
Що `definitions` дають новий обʼєкт на кожен `get()`, а синглтон отримують через `setSingleton()` / секцію `singletons`.
Що `components` це окремий реєстр (`yii\di\ServiceLocator`), обʼєкт там створюється при першому зверненні й далі кешується в `Yii::$app`; `Yii::$container` про нього не знає.
Що контейнер резолвить залежності конструктора рефлексією і кешує ці метадані, а посилання на компонент у конфігу описують через `Instance::of('db')` і розгортають через `Instance::ensure()`.
Розуміння, що в Yii2 обʼєкт налаштовують публічними властивостями з масиву (`Configurable`), а в Laravel через конструктор у провайдері, і що контекстної привʼязки на кшталт `when()->needs()->give()` у Yii2 немає.
Вважати `container.definitions` синглтонами: два `Yii::createObject(PdfRenderer::class)` дадуть два обʼєкти, і кеш усередині сервісу не працюватиме.
Реєструвати будь-який сервіс як компонент застосунку тільки щоб мати `Yii::$app->myService`, а потім складати туди стан, який живе весь запит.
Чекати, що інтерфейс зарезолвиться сам: без визначення контейнер кине `yii\di\NotInstantiableException`.
Писати `new MyService(...)` у коді, а налаштування класу тримати в `container.definitions`, і дивуватись, що воно не застосувалось: визначення діють лише через `Yii::createObject()` / `$container->get()`.
Тримати паролі в `config/web.php` під git замість `*-local.php`, `params` або змінних середовища.
Думати, що `Yii::$app->get('db')` проходить через `Yii::$container`, і намагатись підмінити БД у тесті через `Yii::$container->set('db', ...)`.
ПОРАДА

Скажіть одним реченням: «у Yii2 два реєстри: контейнер за замовчуванням віддає новий обʼєкт, а компоненти застосунку це ліниві синглтони, і склеює їх `Yii::createObject()`». Далі одразу дайте практичний приклад глобальної підміни: `Yii::$container->set(LinkPager::class, ['maxButtonCount' => 5])` змінює всі пейджери, бо віджети створюються через `createObject()`.

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

`Container::get()` кешує обʼєкт лише для того, що зареєстроване через `setSingleton()` або секцію `singletons`. Компоненти ж живуть у `ServiceLocator`: перше звернення до `Yii::$app->db` створює обʼєкт через `Yii::createObject()` і зберігає його в `_components`, далі повертається той самий.