Контейнер инъекции зависимостей
Контейнер инъекции зависимостей (DI) — это объект, который знает, как создавать и настраивать объекты и все их зависимые объекты. В статье Мартина Фаулера хорошо объясняется, почему контейнер DI полезен. Здесь мы в основном объясним использование контейнера DI, предоставляемого Yii.
Инъекция зависимостей
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'),
]);
Информация: Метод yii\di\Container::get() принимает свой третий параметр в виде массива конфигурации, который должен быть применён к создаваемому объекту. Если класс реализует интерфейс yii\base\Configurable (например, yii\base\Object), массив конфигурации будет передан как последний параметр в конструктор класса; в противном случае конфигурация будет применена после создания объекта.
Инъекция PHP-вызова
В этом случае контейнер будет использовать зарегистрированный PHP-вызов для создания новых экземпляров класса. Каждый раз при вызове yii\di\Container::get() будет вызываться соответствующий вызов. Вызов отвечает за разрешение зависимостей и соответствующую инъекцию в недавно созданные объекты. Например,
$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);
Подсказка: Если имя зависимости такое же, как соответствующее определение зависимости, вам не нужно регистрировать его в контейнере DI.
Зависимость, зарегистрированная через 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. Даже если вы указываете массив, например,
'options' => ['id' => 'mypager'], они не будут объединены с другими параметрами, а заменят их.
Другой пример — использовать автоматическую инъекцию конструктора контейнера 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
Подсказка: Контейнер можно настроить в декларативном стиле, используя конфигурацию приложения с версии 2.0.11. См. раздел Конфигурации приложений в руководстве по Конфигурациям.
Всё работает, но в случае необходимости создания класса 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') обозначение. Это означает, что контейнер Container неявно предоставит зависимость, зарегистрированную под именем tempFileStorage, и передаст её в качестве первого аргумента конструктора app\storage\DocumentsWriter.
Примечание: методы setDefinitions() и setSingletons() доступны начиная с версии 2.0.11.
Ещё один шаг по оптимизации конфигурации — регистрация некоторых зависимостей в качестве синглетонов. Зависимость, зарегистрированная через 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. Последний автоматически разрешит зависимости, как описано выше.
© 2008–2017 by Yii Software LLC
Licensed under the three clause BSD license.
http://www.yiiframework.com/doc-2.0/guide-concept-di-container.html