Контейнер внедрения зависимости

Контейнер инъекции зависимостей (DI) - это объект, который знает, как создавать и настраивать объекты и все их зависимые объекты. 

Внедрение зависимости

Yii предоставляет возможность контейнера DI через класс yii\di\Container. Он поддерживает следующие типы инъекций зависимостей:

  • Конструктор инъекции;
  • Метод инъекции;
  • Сеттер и инъекция свойств;
  • PHP вызываемая инъекция;

Конструктор инъекций

Контейнер DI поддерживает вставку конструктора с помощью подсказок типа для параметров конструктора. Подсказки типа указывают контейнеру, какие классы или интерфейсы зависимы, когда он используется для создания нового объекта. Контейнер попытается получить экземпляры зависимых классов или интерфейсов, а затем вставить их в новый объект через конструктор. Например:

class Foo
{
    public function __construct(Bar $bar)
    {
    }
}

$foo = $container->get('Foo');
// which is equivalent to the following:
$bar = new Bar;
$foo = new Foo($bar);

Метод инъекции

Обычно зависимости класса передаются конструктору и доступны внутри класса на протяжении всего жизненного цикла. С методом инъекции метода можно предоставить зависимость, которая нужна только одному методу класса, и передать его конструктору может быть невозможно или может привести к слишком большим издержкам в большинстве случаев использования.

Метод класса может быть определен как метод doSomething() в следующем примере:

class MyClass extends \yii\base\Component
{
    public function __construct(/*Some lightweight dependencies here*/, $config = [])
    {
        // ...
    }

    public function doSomething($param1, \my\heavy\Dependency $something)
    {
        // do something with $something
    }
}

Вы можете вызвать этот метод, передав экземпляр \my\heavy\Dependency самостоятельно или используя yii\di\Container::invoke() следующим образом:

$obj = new MyClass(/*...*/);
Yii::$container->invoke([$obj, 'doSomething'], ['param1' => 42]); // $something will be provided by the DI container

Сеттер и инъекция свойств

Конфигурации поддерживаются посредством вставки сеттеров и свойств. При регистрации зависимостей или при создании нового объекта вы можете предоставить конфигурацию, которая будет использоваться контейнером для вставки зависимостей с помощью соответствующих установщиков или свойств. Например:

use yii\base\Object;

class Foo extends Object
{
    public $bar;

    private $_qux;

    public function getQux()
    {
        return $this->_qux;
    }

    public function setQux(Qux $qux)
    {
        $this->_qux = $qux;
    }
}

$container->get('Foo', [], [
    'bar' => $container->get('Bar'),
    'qux' => $container->get('Qux'),
]);

Вызываемая PHP-инъекция

В этом случае контейнер будет использовать зарегистрированный PHP, который можно вызвать для создания новых экземпляров класса. Каждый раз, когда вызывается yii\di\Container::get(), будет вызван соответствующий callable. Вызываемый отвечает за разрешение зависимостей и их правильное встраивание в новые объекты. Например:

$container->set('Foo', function () {
    $foo = new Foo(new Bar);
    // ... other initializations ...
    return $foo;
});

$foo = $container->get('Foo');

Чтобы скрыть сложную логику построения нового объекта, вы можете использовать статический метод класса как вызываемый. Например:

class FooBuilder
{
    public static function build()
    {
        $foo = new Foo(new Bar);
        // ... other initializations ...
        return $foo;
    }
}

$container->set('Foo', ['app\helper\FooBuilder', 'build']);

$foo = $container->get('Foo');

Поступая таким образом, человек, который хочет настроить класс Foo, больше не должен знать, как он строится.

Регистрация зависимостей

Вы можете использовать yii\di\Container::set() для регистрации зависимостей. Для регистрации требуется имя зависимости, а также определение зависимости. Имя зависимости может быть именем класса, именем интерфейса или псевдонимом; И определение зависимости может быть именем класса, массивом конфигурации или PHP-вызовом.

$container = new \yii\di\Container;

// register a class name as is. This can be skipped.
$container->set('yii\db\Connection');

// register an interface
// When a class depends on the interface, the corresponding class
// will be instantiated as the dependent object
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

// register an alias name. You can use $container->get('foo')
// to create an instance of Connection
$container->set('foo', 'yii\db\Connection');

// register a class with configuration. The configuration
// will be applied when the class is instantiated by get()
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// register an alias name with class configuration
// In this case, a "class" element is required to specify the class
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// register a PHP callable
// The callable will be executed each time when $container->get('db') is called
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});

// register a component instance
// $container->get('pageCache') will return the same instance each time it is called
$container->set('pageCache', new FileCache);

Зависимость, зарегистрированная с помощью set(), будет генерировать экземпляр каждый раз, когда потребуется зависимость. Вы можете использовать yii\di\Container::setSingleton() для регистрации зависимости, которая генерирует только один экземпляр:

$container->setSingleton('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

Разрешение зависимостей

После регистрации зависимостей вы можете использовать контейнер DI для создания новых объектов, а контейнер автоматически будет разрешать зависимости, создавая экземпляры и вставляя их во вновь создаваемые объекты. Разрешение зависимостей является рекурсивным, то есть, если зависимость имеет другие зависимости, эти зависимости также будут автоматически разрешаться.

Вы можете использовать get() для создания или получения экземпляра объекта. Метод принимает имя зависимости, которое может быть именем класса, именем интерфейса или псевдонимом. Имя зависимости может быть зарегистрировано через set() или setSingleton(). Вы можете опционально предоставить список параметров конструктора класса и конфигурацию для настройки вновь созданного объекта.

Например:

// "db" is a previously registered alias name
$db = $container->get('db');

// equivalent to: $engine = new \app\components\SearchEngine($apiKey, $apiSecret, ['type' => 1]);
$engine = $container->get('app\components\SearchEngine', [$apiKey, $apiSecret], ['type' => 1]);

За сценой контейнер DI выполняет гораздо больше работы, чем просто создание нового объекта. Контейнер сначала проверяет конструктор класса, чтобы найти зависимые имена классов или интерфейсов, а затем автоматически решает эти зависимости.

Следующий код показывает более сложный пример. Класс UserLister зависит от объекта, реализующего интерфейс UserFinderInterface; Класс UserFinder реализует этот интерфейс и зависит от объекта Connection. Все эти зависимости объявляются через указание типов параметров конструктора класса. При регистрации зависимостей свойств контейнер DI может автоматически разрешать эти зависимости и создает новый экземпляр UserLister с простым вызовом get('userLister').

namespace app\models;

use yii\base\Object;
use yii\db\Connection;
use yii\di\Container;

interface UserFinderInterface
{
    function findUser();
}

class UserFinder extends Object implements UserFinderInterface
{
    public $db;

    public function __construct(Connection $db, $config = [])
    {
        $this->db = $db;
        parent::__construct($config);
    }

    public function findUser()
    {
    }
}

class UserLister extends Object
{
    public $finder;

    public function __construct(UserFinderInterface $finder, $config = [])
    {
        $this->finder = $finder;
        parent::__construct($config);
    }
}

$container = new Container;
$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');

// which is equivalent to:

$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);

Практическое использование

Yii создает контейнер DI, когда вы включаете файл Yii.php в сценарий входа вашего приложения. Контейнер DI доступен через контейнер Yii::$container. Когда вы вызываете Yii::createObject(), метод фактически вызовет метод get() контейнера для создания нового объекта. Как уже упоминалось, контейнер DI автоматически разрешает зависимости (если они есть) и вставляет их в полученный объект. Поскольку Yii использует Yii::createObject() в большинстве своих базовых программ для создания новых объектов, это означает, что вы можете настраивать объекты глобально, обращаясь к контейнеру Yii::$container.

Например, давайте настроим глобально число кнопок разбивки по умолчанию в yii\widgets\LinkPager.

\Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);

Теперь, если вы используете виджет в представлении со следующим кодом, свойство maxButtonCount будет инициализировано как 5 вместо значения по умолчанию 10, определенного в классе.

echo \yii\widgets\LinkPager::widget();

Однако вы можете переопределить значение, установленное через контейнер DI, хотя:

echo \yii\widgets\LinkPager::widget(['maxButtonCount' => 20]);

Другим примером является использование автоматической инъекции конструктора в контейнер DI. Предположим, что ваш класс контроллера зависит от некоторых других объектов, таких как служба бронирования отелей. Вы можете объявить зависимость через параметр конструктора и позволить контейнеру DI решать его за вас.

namespace app\controllers;

use yii\web\Controller;
use app\components\BookingInterface;

class HotelController extends Controller
{
    protected $bookingService;

    public function __construct($id, $module, BookingInterface $bookingService, $config = [])
    {
        $this->bookingService = $bookingService;
        parent::__construct($id, $module, $config);
    }
}

Если вы перейдете к этому контроллеру из браузера, вы увидите сообщение об ошибке, сообщающее, что метод BookingInterface не может быть создан. Это объясняется тем, что вам нужно указать контейнеру DI, как справиться с этой зависимостью:

\Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService');

Теперь, если вы снова обращаетесь к контроллеру, экземпляр app\components\BookingService будет создан и введен в качестве третьего параметра конструктора контроллера.

Продвинутое практическое использование

Предположим, мы работаем над приложением API и имеем:

  • app\components\Request класс, который расширяет yii\web\Request и предоставляет дополнительные функции
  • app\components\Response класс, который расширяет yii\web\Response и должен иметь свойство format, установленное на json при создании
  • app\storage\FileStorage и app\storage\DocumentsReader, которые реализуют некоторую логику работы с документами, расположенными в каком-то хранилище файлов:
class FileStorage
{
    public function __contruct($root) {
        // whatever
    }
}
  
class DocumentsReader
{
    public function __contruct(FileStorage $fs) {
        // whatever
    }
}

Можно настроить сразу несколько определений, передавая массив конфигурации методу setDefinitions() или setSingletons(). Итерируя массив конфигурации, методы будут вызывать set() или setSingleton() соответственно для каждого элемента.

Формат массива конфигурации:

  • key: имя класса, имя интерфейса или псевдоним. Ключ будет передан методу set() в качестве первого аргумента $class.
  • value: определение, связанное с $class. Возможные значения описаны в документации set() для параметра $definition. Будет передан методу set() в качестве второго аргумента $definition.

Например, давайте сконфигурируем наш контейнер для выполнения вышеупомянутых требований:

$container->setDefinitions([
    'yii\web\Request' => 'app\components\Request',
    'yii\web\Response' => [
        'class' => 'app\components\Response',
        'format' => 'json'
    ],
    'app\storage\DocumentsReader' => function () {
        $fs = new app\storage\FileStorage('/var/tempfiles');
        return new app\storage\DocumentsReader($fs);
    }
]);

$reader = $container->get('app\storage\DocumentsReader); 
// Will create DocumentReader object with its dependencies as described in the config 

Все работает, но в случае, если нам нужно создать класс DocumentWriter, мы скопируем-вставим строку, которая создает объект FileStorage, что, конечно, не самый умный способ.

set() и setSingleton() могут необязательно использовать параметры конструктора зависимостей в качестве третьего аргумента. Чтобы задать параметры конструктора, вы можете использовать следующий формат массива конфигурации:

  • key: имя класса, имя интерфейса или псевдоним. Ключ будет передан методу set() в качестве первого аргумента $class.
  • value: массив из двух элементов. Первый элемент будет передан методу set() в качестве второго аргумента $definition, второй - как $params.

Давайте изменим наш пример:

$container->setDefinitions([
    'tempFileStorage' => [ // we've created an alias for convenience
        ['class' => 'app\storage\FileStorage'],
        ['/var/tempfiles'] // could be extracted from some config files
    ],
    'app\storage\DocumentsReader' => [
        ['class' => 'app\storage\DocumentsReader'],
        [Instance::of('tempFileStorage')]
    ],
    'app\storage\DocumentsWriter' => [
        ['class' => 'app\storage\DocumentsWriter'],
        [Instance::of('tempFileStorage')]
    ]
]);

$reader = $container->get('app\storage\DocumentsReader); 
// Will behave exactly the same as in the previous example.

Вы можете заметить нотацию Instance::of('tempFileStorage'). Это означает, что Контейнер будет неявно обеспечивать зависимость, зарегистрированную под именем tempFileStorage, и передать ее в качестве первого аргумента конструктора app\storage\DocumentsWriter.

Другим шагом по оптимизации конфигурации является регистрация некоторых зависимостей в качестве однопоточных. Зависимость, зарегистрированная через set(), будет создаваться каждый раз, когда это необходимо. Некоторые классы не изменяют состояние во время выполнения, поэтому они могут быть зарегистрированы как одиночные, чтобы повысить производительность приложения.

Хорошим примером может быть класс app\storage\FileStorage, который выполняет некоторые операции с файловой системой с помощью простого API (например, $fs->read(), $fs->write()). Эти операции не изменяют внутреннее состояние класса, поэтому мы можем создать его экземпляр один раз и использовать его несколько раз.

$container->setSingletons([
    'tempFileStorage' => [
        ['class' => 'app\storage\FileStorage'],
        ['/var/tempfiles']
    ],
]);

$container->setDefinitions([
    'app\storage\DocumentsReader' => [
        ['class' => 'app\storage\DocumentsReader'],
        [Instance::of('tempFileStorage')]
    ],
    'app\storage\DocumentsWriter' => [
        ['class' => 'app\storage\DocumentsWriter'],
        [Instance::of('tempFileStorage')]
    ]
]);

$reader = $container->get('app\storage\DocumentsReader');

Когда регистрировать зависимости

Поскольку при создании новых объектов требуются зависимости, их регистрация должна выполняться как можно раньше. Рекомендуемая практика:

  • Если вы являетесь разработчиком приложения, вы можете зарегистрировать свои зависимости, используя конфигурацию приложения. 
  • Если вы разработчик распространяемого расширения, вы можете зарегистрировать зависимости в классе начальной загрузки расширения.

И инжекция зависимостей, и локатор сервисов являются популярными шаблонами проектирования, которые позволяют создавать программное обеспечение в свободно-связанной и более тестируемой форме. Yii реализует свой локатор сервиса поверх контейнера впрыска зависимостей (DI). Когда локатор службы пытается создать новый экземпляр объекта, он переадресует вызов в контейнер DI.