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

Контейнер внедрения зависимостей - это объект, который знает, как создать и настроить экземпляр объекта и зависимых от него объектов. Статья Мартина Фаулера хорошо объясняет, почему контейнер внедрения зависимостей является полезным. Здесь, преимущественно, будет объясняться использование контейнера внедрения зависимостей, предоставляемого в Yii.

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

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

  • Внедрение зависимости через конструктор.
  • Внедрение зависимости через сеттер и свойство.
  • Внедрение зависимости через PHP callback.

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

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

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

$foo = $container->get('Foo');
// что равносильно следующему:
$bar = new Bar;
$foo = new Foo($bar);

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

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

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 callback

В данном случае, контейнер будет использовать зарегистрированный PHP callback для создания новых экземпляров класса. Callback отвечает за разрешения зависимостей и внедряет их в соответствии с вновь создаваемыми объектами. Например,

$container->set('Foo', function () {
    return new Foo(new Bar);
});

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

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

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

$container = new \yii\di\Container;

// регистрация имени класса, как есть. это может быть пропущено.
$container->set('yii\db\Connection');

// регистрация интерфейса
// Когда класс зависит от интерфейса, соответствующий класс
// будет использован в качестве зависимости объекта
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

// регистрация алиаса. Вы можете использовать $container->get('foo')
// для создания экземпляра Connection
$container->set('foo', 'yii\db\Connection');

// Регистрация класса с конфигурацией. Конфигурация
// будет применена при создании экземпляра класса через get()
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// регистрация алиаса с конфигурацией класса
// В данном случае, параметр "class" требуется для указания класса
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// регистрация PHP callback'a
// Callback будет выполняться каждый раз при вызове $container->get('db')
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});

// регистрация экземпляра компонента
// $container->get('pageCache') вернёт тот же экземпляр при каждом вызове
$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',
]);

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

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

Вы можете использовать [[yii\di\Container::get()]] для создания новых объектов. Метод принимает имя зависимости, которым может быть имя класса, имя интерфейса или псевдоним. Имя зависимости может быть или не может быть зарегистрировано через set() или setSingleton(). Вы можете опционально предоставить список параметров конструктора класса и конфигурацию для настройки созданного объекта. Например,

// "db" ранее зарегистрированный псевдоним
$db = $container->get('db');

// эквивалентно: $engine = new \app\components\SearchEngine($apiKey, ['type' => 1]);
$engine = $container->get('app\components\SearchEngine', [$apiKey], ['type' => 1]);

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

Следующий код демонстрирует более сложный пример. Класс UserLister зависит от объекта, реализующего интерфейс UserFinderInterface; класс UserFinder реализует этот интерфейс и зависит от объекта Connection. Все эти зависимости были объявлены через тип подсказки параметров конструктора класса. При регистрации зависимости через свойство, контейнер внедрения зависимостей позволяет автоматически разрешить эти зависимости и создаёт новый экземпляр 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');

// что эквивалентно:

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

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

Yii создаёт контейнер внедрения зависимостей когда вы подключаете файл Yii.php во входном скрипте вашего приложения. Контейнер внедрения зависимостей доступен через [[Yii::$container]]. При вызове [[Yii::createObject()]], метод на самом деле вызовет метод контейнера [[yii\di\Container::get()|get()]], чтобы создать новый объект. Как упомянуто выше, контейнер внедрения зависимостей автоматически разрешит зависимости (если таковые имеются) и внедрит их в только что созданный объект. Поскольку Yii использует [[Yii::createObject()]] в большей части кода своего ядра для создания новых объектов, это означает, что вы можете настроить глобальные объекты, имея дело с [[Yii::$container]].

Например, вы можете настроить по умолчанию глобальное количество кнопок в пейджере [[yii\widgets\LinkPager]]:

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

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

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

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

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

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

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

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

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

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

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

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

Итог

Как dependency injection, так и service locator являются популярными паттернами проектирования, которые позволяют создавать программное обеспечение в слабосвязанной и более тестируемой манере. Мы настоятельно рекомендуем к прочтению статью Мартина Фаулера, для более глубокого понимания dependency injection и service locator.

Yii реализует свой service locator поверх контейнера внедрения зависимостей. Когда service locator пытается создать новый экземпляр объекта, он перенаправляет вызов на контейнер внедрения зависимостей. Последний будет разрешать зависимости автоматически, как описано выше.