2 · курс
0%
Главная/🧠Ключевые концепции

22Service Locator и DI-контейнер

Yii::$app как локатор сервисов, Yii::$container как контейнер внедрения зависимостей: регистрация, разрешение через конструктор и сеттеры, синглтоны, Instance::of(), практические сценарии.

⏱ ~5 мин 📚 2 главы руководства

Service Locator (Yii::$app, любой модуль) хранит именованные компоненты и отдаёт их по ID. DI-контейнер (Yii::$container) умеет создавать объекты, сам подставляя зависимости из типов конструктора. Локатор построен поверх контейнера: когда Yii::$app->db создаётся впервые, объект собирает контейнер.

Service Locator#

PHP
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() локатора. Для обёрток над сторонними библиотеками удобно прятать сложную инициализацию за статической фабрикой, которая возвращает замыкание:

PHP
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-контейнер#

Виды внедрения#

PHP
// 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());
});

Регистрация#

PHP
$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() — один раз.

Разрешение#

PHP
$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]);   // именованные параметры

Разрешение рекурсивно: контейнер смотрит на типы параметров конструктора и создаёт всю цепочку.

PHP
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()#

PHP
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.

Практика#

Умолчания для виджетов и любых классов#

PHP
Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);
echo LinkPager::widget();                          // 5 кнопок
echo LinkPager::widget(['maxButtonCount' => 20]);  // явное значение сильнее

Работает для всего, что создаётся через Yii::createObject() — а это почти всё ядро.

Зависимости в контроллере#

PHP
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». Регистрируем реализацию — и контейнер подставит её третьим аргументом:

PHP
Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService');
Проверь себя5

Локатор отдаёт компоненты по строковому ID (Yii::$app->db); контейнер создаёт объекты по классу/интерфейсу, автоматически разрешая зависимости конструктора. Локатор использует контейнер для создания своих компонентов.

Когда объект не хранит изменяемого состояния и его дорого создавать — например, обёртка над файловым хранилищем или соединение.

Чтобы в параметрах конструктора сослаться на другую зарегистрированную зависимость по имени — контейнер подставит её при создании.

Ошибка создания: контейнер не знает, какой класс подставить. Нужно Yii::$container->set(Interface::class, Impl::class).

Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]) или container.definitions в конфигурации.