22Service Locator и DI-контейнер
Yii::$app как локатор сервисов, Yii::$container как контейнер внедрения зависимостей: регистрация, разрешение через конструктор и сеттеры, синглтоны, Instance::of(), практические сценарии.
Service Locator (Yii::$app, любой модуль) хранит именованные компоненты и отдаёт их по ID. DI-контейнер (Yii::$container) умеет создавать объекты, сам подставляя зависимости из типов конструктора. Локатор построен поверх контейнера: когда Yii::$app->db создаётся впервые, объект собирает контейнер.
Service Locator#
use yii\di\ServiceLocator;
$locator = new ServiceLocator();
$locator->set('cache', 'yii\caching\ApcCache'); // имя класса
$locator->set('db', ['class' => 'yii\db\Connection', 'dsn' => '…']); // конфигурация
$locator->set('search', function () { return new SolrService('127.0.0.1'); }); // фабрика
$locator->set('pageCache', new FileCache()); // готовый объект
$cache = $locator->get('cache'); // создаётся при первом обращении, дальше тот же экземпляр
$cache = $locator->cache; // то же самое — через магическое свойство
$locator->has('cache'); // зарегистрирован ли
$locator->get('nope'); // исключениеСвойство components в конфигурации приложения — это setComponents() локатора. Для обёрток над сторонними библиотеками удобно прятать сложную инициализацию за статической фабрикой, которая возвращает замыкание:
class SolrServiceBuilder
{
public static function build($ip)
{
return function () use ($ip) {
$solr = new app\components\SolrService($ip);
// ...инициализация...
return $solr;
};
}
}
'components' => ['search' => SolrServiceBuilder::build('127.0.0.1')],DI-контейнер#
Виды внедрения#
// 1. Через конструктор: контейнер видит тип и создаёт зависимость сам
class Foo
{
public function __construct(Bar $bar) { }
}
$foo = $container->get('Foo'); // == new Foo(new Bar())
// 2. Через метод: зависимость нужна одному методу
Yii::$container->invoke([$obj, 'doSomething'], ['param1' => 42]); // остальные параметры по типам
// 3. Через сеттер/свойство: конфигурация третьим аргументом
$container->get('Foo', [], ['bar' => $container->get('Bar'), 'qux' => $container->get('Qux')]);
// 4. Через callback: полный контроль над созданием
$container->set('Foo', function ($container, $params, $config) {
return new Foo(new Bar());
});Регистрация#
$container = Yii::$container;
$container->set('yii\db\Connection'); // класс как есть (можно не регистрировать)
$container->set('yii\mail\MailerInterface', 'yii\symfonymailer\Mailer'); // интерфейс → реализация
$container->set('foo', 'yii\db\Connection'); // псевдоним
$container->set('yii\db\Connection', ['dsn' => '…', 'username' => 'root']); // класс + конфигурация
$container->set('db', ['class' => 'yii\db\Connection', 'dsn' => '…']); // псевдоним + конфигурация
$container->set('pageCache', new FileCache()); // экземпляр
$container->setSingleton('yii\db\Connection', ['dsn' => '…']); // один экземпляр на всё приложениеset() создаёт новый объект при каждом get(), setSingleton() — один раз.
Разрешение#
$db = $container->get('db');
$engine = $container->get('app\components\SearchEngine', [$apiKey, $apiSecret], ['type' => 1]);
// == new SearchEngine($apiKey, $apiSecret, ['type' => 1])
$api = $container->get('app\components\Api', ['host' => $host, 'apiKey' => $apiKey]); // именованные параметрыРазрешение рекурсивно: контейнер смотрит на типы параметров конструктора и создаёт всю цепочку.
interface UserFinderInterface { function findUser(); }
class UserFinder extends BaseObject implements UserFinderInterface
{
public $db;
public function __construct(Connection $db, $config = [])
{
$this->db = $db;
parent::__construct($config);
}
public function findUser() { }
}
class UserLister extends BaseObject
{
public $finder;
public function __construct(UserFinderInterface $finder, $config = [])
{
$this->finder = $finder;
parent::__construct($config);
}
}
$container->set('yii\db\Connection', ['dsn' => '...']);
$container->set('app\models\UserFinderInterface', ['class' => 'app\models\UserFinder']);
$container->set('userLister', 'app\models\UserLister');
$lister = $container->get('userLister');
// == $db = new Connection(['dsn' => '...']); $finder = new UserFinder($db); $lister = new UserLister($finder);Массовая регистрация и Instance::of()#
use yii\di\Instance;
$container->setSingletons([
'tempFileStorage' => [
['class' => 'app\storage\FileStorage'], // определение
['/var/tempfiles'], // параметры конструктора
],
]);
$container->setDefinitions([
'yii\web\Request' => 'app\components\Request',
'yii\web\Response' => ['class' => 'app\components\Response', 'format' => 'json'],
'app\storage\DocumentsReader' => [
['class' => 'app\storage\DocumentsReader'],
[Instance::of('tempFileStorage')], // ссылка на зарегистрированную зависимость
],
]);То же самое декларативно в конфигурации приложения — свойство container с ключами definitions и singletons.
Практика#
Умолчания для виджетов и любых классов#
Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);
echo LinkPager::widget(); // 5 кнопок
echo LinkPager::widget(['maxButtonCount' => 20]); // явное значение сильнееРаботает для всего, что создаётся через Yii::createObject() — а это почти всё ядро.
Зависимости в контроллере#
namespace app\controllers;
use app\components\BookingInterface;
use yii\web\Controller;
class HotelController extends Controller
{
protected $bookingService;
public function __construct($id, $module, BookingInterface $bookingService, $config = [])
{
$this->bookingService = $bookingService;
parent::__construct($id, $module, $config);
}
}Без регистрации будет ошибка «cannot instantiate BookingInterface». Регистрируем реализацию — и контейнер подставит её третьим аргументом:
Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService');Локатор отдаёт компоненты по строковому ID (Yii::$app->db); контейнер создаёт объекты по классу/интерфейсу, автоматически разрешая зависимости конструктора. Локатор использует контейнер для создания своих компонентов.
Когда объект не хранит изменяемого состояния и его дорого создавать — например, обёртка над файловым хранилищем или соединение.
Чтобы в параметрах конструктора сослаться на другую зарегистрированную зависимость по имени — контейнер подставит её при создании.
Ошибка создания: контейнер не знает, какой класс подставить. Нужно Yii::$container->set(Interface::class, Impl::class).
Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]) или container.definitions в конфигурации.