Контейнер инъекции зависимостей (\Phact\Di\Container) необходим для инъекции зависимостей в ваш сервис/компонент c целью уменьшения связанности кода и распределения обязанностей.
Представим, что у нас есть компонент, который работает с запросом (request). Например, основной его задачей является вывод метода запроса. Можно воспользоваться простым доступом к сервис-локатору через обращение к приложению. Например, как то так:
<?php
use Phact\Main\Phact;
class AmazingComponent
{
public function dumpRequestMethod()
{
echo "Request method is: " . Phact::app()->request->getMethod();
}
}
Всё вроде неплохо, за исключением того, что:
- Наш компонент будет сложно тестировать (для этого придется воспроизводить всё окружение, вместе с приложением)
- Он привязан ко всему приложению, а не только к объекту запроса
- Мы не до конца уверены в том, что нам вернет конструкция
Phact::app()->request
, вполне возможно что там окажется не то, чего бы мы хотели
Как можно решить данную проблему?
Очевидно, что наш компонент без объекта запроса не сможет выполнять своё предназначение - следовательно, AmazingComponent зависит от HttpRequest. Или иначе - HttpRequest является зависимостью компонента AmazingComponent.
Так, с зависимостями всё понятно, что такое инъекция?
В данном контексте инъекция - это "встраивание" зависимости в зависимый объект. Она может осуществлятся различными методами:
- Инъекция через конструктор
- Инъекция через "сеттер"
Самое очевидное - если зависимый компонент не может выполнять своих основных задач без зависимости, то он без нее не может существовать, следовательно - создать зависимый компонент без зависимости невозоможно.
Отсюда вытекает, что зависимости необходимо передавать в конструктор зависимого объекта. В нашем случае это выглядело бы так:
<?php
use Phact\Request\HttpRequest;
class AmazingComponent
{
private $request;
public function __construct(HttpRequest $request)
{
$this->request = $request;
}
public function dumpRequestMethod()
{
echo "Request method is: " . $this->request->getMethod();
}
}
Отлично, мы практически решили все свои проблемы - мы больше не зависим от приложения, да и тестировать наш компонент будет достаточно просто - достаточно передать в качестве зависимости заранее подготовленный объект HttpRequest.
Но мы всё еще завязаны на реализации конкретного класса HttpRequest, что может быть весьма неудобным как минимум при подготовке тестовых объектов. Чтобы отвязаться от реализации, необходимо указывать в качестве зависимостей интерфейсы - это решит несколько задач:
- Зависимый компонент будет знать чего ожидать от зависимости, какие методы она реализует
- Зависимость может реализовывать методы любыми способами, то есть быть независимой от конкретных реализаций
Скорректируем наш компонент:
<?php
use Phact\Request\HttpRequestInterface;
class AmazingComponent
{
private $request;
public function __construct(HttpRequestInterface $request)
{
$this->request = $request;
}
public function dumpRequestMethod()
{
echo "Request method is: " . $this->request->getMethod();
}
}
Теперь наш компонент не зависит от реализации объекта зависимости, но всегда знает как с ним работать. Amazing!
Инъекция через сеттер может быть необходима в некоторых случаях:
- Чтобы работать с опциональными зависимостями
- Чтобы избежать циркулярных (круговых) зависимостей
Предположим, что в наш компонент необходимо добавить опциональное логирование. Логирование в данном случае не является основной задачей нашего компонента и без него он должен выполнять основную задачу.
В данном случае отлично подойдет инъекция через сеттер. Добавим логирование в наш компонент:
<?php
use Phact\Request\HttpRequestInterface;
use Psr\Log\LoggerInterface;
class AmazingComponent
{
private $request;
private $logger;
public function __construct(HttpRequestInterface $request)
{
$this->request = $request;
}
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function dumpRequestMethod()
{
if ($this->logger) {
$this->logger->debug("Before output request method");
}
echo "Request method is: " . $this->request->getMethod();
if ($this->logger) {
$this->logger->debug("After output request method");
}
}
}
Контейнер - это сущность, которая помогает совершать инъекцию зависимостей в компоненты. В задачи контейнера входит автоматическое обнаружение зависимостей и их инъекция при создании компонента, а так же дополнительные полезные возможности после создания компонента (вызовы методов, установка свойств).
Рассмотрим простой пример конфигурации одного компонента без зависимостей;
[
'standalone' => [
'class' => StandaloneComponent::class
]
]
или (аналогично примеру выше)
[
'standalone' => StandaloneComponent::class
]
Если данную конфигурацию загрузить в контейнер, то при запросе компонента $container->get('standalone');
контейнер создаст и вернет нам объект класса StandaloneComponent,
при этом при следующем обращении к контейнеру за этим же компонентом на вернется тот же самый объект,
то есть инициализация (создание) компонента производится всего один раз.
Попробуем описать конфигурацию для нашего примера:
[
'request' => [
'class' => \Phact\Request\Request::class
],
'amazing' => [
'class' => AmazingComponent::class
]
]
Теперь, при запросе компонента $container->get('amazing');
нам вернется объект класса AmazingComponent,
в который уже произведена инъекция компонента request.
Контейнер сам обнаружил в конфигурации подходящий компонент,
создал его и затем создал зависимый компонент amazing, подставив необходимый объект в конструктор.
Просто прекрасно!
Стоп, а если у нас два компонента класса Request, какой из них подставится? По-умолчанию будет подставлен тот, который выше описан в конфигурации, но если нам необходимо указать на конкретный компонент, сделать это можно с помощью установки аргументов конструктора и ссылки на компонент:
[
'request' => [
'class' => \Phact\Request\Request::class
],
'my_request' => [
'class' => \Phact\Request\Request::class
],
'amazing' => [
'class' => AmazingComponent::class,
'arguments' => [
'@my_request'
]
]
]
Для аргументов конструктора и аргументов методов можно использовать ссылки на компоненты, они являются строками, начинающимися с символа '@' и содержащие идентификатор компонента
Виды ссылок:
- Ссылка на обязательный компонент, начинается с '@'
- Ссылка на опциональный компонент, начинается с '@?'
- Ссылка на загруженный компонент, начинается с '@!'
При обнаружении ссылки на обязательный компонент в аргументах конструктора или метода контейнер будет искать и создавать необходимые компоненты, если он не сможет найти подходящие компоненты, то будет сгенерировано исключение \Phact\Exceptions\NotFoundContainerException
При обнаружении ссылки на опциональный компонент в аргументах конструктора или метода контейнер будет искать и создавать необходимые компоненты, если он не сможет найти подходящие компоненты, то вместо них будет подставлено значение null
Поведение данной ссылки отличается в зависимости от того, где она была указана - в аргументах конструктора или метода.
Поведение контейнера при указании ссылки в аргументах конструктора: если компонент, на который указана ссылка ранее был инициализирован, то будет подставлен он, в противном случае будет подставлено значение null
Поведение контейнера при указании ссылки в аргументах метода: если компонент, на который указана ссылка ранее был инициализирован, то будет подставлен он, в противном случае выполнение метода будет отложено до момента инициализации компонента, на который указана ссылка. После инициализации компонента, на который указана ссылка, "отложенный" метод будет выполнен.
Аргументы конструктора могут быть не только ссылками на компоненты, но и содержать любые значения, необходимые для создания объекта или выполнения метода. Аргументы могут указываться как массив по позициям элементов в методе, например:
[
'amazing' => [
'class' => AmazingComponent::class,
'arguments' => [
'@my_request'
]
]
]
Либо как массив ключ-значение, где ключом является имя аргумента:
[
'amazing' => [
'class' => AmazingComponent::class,
'arguments' => [
'request' => '@my_request'
]
]
]
Можно так же указывать только те параметры, которые мы хотим задать самостоятельно, остальные параметры контейнер будет подставлять сам.
Но, а как же логгер?
Обратимся к другой приятной возможности контейнера - вызовам методов
[
'request' => [
'class' => \Phact\Request\Request::class
],
'logger' => [
'class' => \Phact\Request\Request::class
],
'amazing' => [
'class' => AmazingComponent::class,
'calls' => [
'setLogger'
]
]
]
Теперь логгер будет инъектирован в наш компонент автоматически, но при вызовах методов так же можно явно указать ссылку на необходимый компонент:
[
'request' => [
'class' => \Phact\Request\Request::class
],
'logger' => [
'class' => \Phact\Request\Request::class
],
'amazing' => [
'class' => AmazingComponent::class,
'calls' => [
'setLogger' => ['@logger']
]
]
]
А если нам необходимо устанавливать логгер в наш компонент только после инициализации логгера, можно указать ссылку на загруженный компонент:
[
'request' => [
'class' => \Phact\Request\Request::class
],
'logger' => [
'class' => \Phact\Request\Request::class
],
'amazing' => [
'class' => AmazingComponent::class,
'calls' => [
'setLogger' => ['@!logger']
]
]
]
При необходимости вызова одного и того же метода несколько раз можно применять следующую структуру:
[
'new' => [
'class' => SomeNewComponent::class,
'calls' => [
[
'method' => 'appendLogger',
'arguments' => ['@logger']
],
[
'method' => 'appendLogger',
'arguments' => ['@another_logger']
]
]
]
]
Еще одной удобной особенностью контейнера является возможность установки свойств компонентов.
Добавим в наш компонент какое-нибудь публичное свойство:
<?php
...
class AmazingComponent
{
...
public $name;
...
}
И установим его через контейнер:
[
'request' => [
'class' => \Phact\Request\Request::class
],
'logger' => [
'class' => \Phact\Request\Request::class
],
'amazing' => [
'class' => AmazingComponent::class,
'calls' => [
'setLogger' => ['@logger']
],
'properties' => [
'name' => 'My amazing component'
]
]
]
Теперь при инициализации нашего компонента свойства, указанные в конфигурации, будут устанавливаться автоматически.
DI Container встроен в Phact на уровне приложения, это позволяет использовать все преимущества DI в том числе и в стандартных объектах проекта - контроллерах, модулях, командах.
Конфигурирование компонентов происходит в разделе components
конфигурационного файла, кроме этого можно переопределить стандартный DI Container своим. Для этого необходимо указать в конфигурационном
файле раздел container
с указанием собственного класса контейнера:
[
...
'container' => [
'class' => \My\Mega\DiContainer
],
'components' => [
...
]
...
]
Именно с этого мы и начали - поэтому подробно останавливаться на этом я не буду, выше уже есть масса примеров.
Через контейнер создается объект контроллера и выполняется action, поэтому в обоих случаях можно использовать DI.
Описываем в конструкторе собственного контроллера зависимость от необходимого компонента. Например, нам потребовался роутер:
...
class MyController extends Controller
{
protected $router;
public function __construct(RouterInterface $router, HttpRequestInterface $request, RendererInterface $renderer = null)
{
$this->_router = $router;
parent::__construct($request, $renderer);
}
...
}
Ну и не забываем о том, что базовому контроллеру тоже необходимы зависимости (HttpRequestInterface
, RendererInterface
).
Инъекция через конструктор контроллера - удобная вещь, однако у этого подхода есть минусы:
- Зависимость может использоваться всего лишь в небольшом количестве действий (action) и при использовании большого набора зависимостей в разных действиях (action) конструктор значительно "распухнет"
- Зависимость может быть достаточно "тяжелой", для того, чтобы вызывать ее для каждого действия (action)
В данных случаях лучше всего воспользоваться инъекцией через action:
...
class MyController extends Controller
{
public function index(RouterInterface $router)
{
...
}
...
}
Использование действий (action) с параметрами:
...
class MyController extends Controller
{
public function view($id, RouterInterface $router)
{
...
}
...
}
Аналогично инъекции через конструктор контроллера:
...
class MyCommand extends Command
{
protected $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
...
}
Аналогично инъекции через действие (action) контроллера:
...
class MyCommand extends Command
{
public function handle($args = [], RouterInterface $router)
{
...
}
...
}
Инъекция зависимостей в объект модуля осуществляется через конструктор:
...
class MyModule extends Module
{
protected $router;
public function __construct(string $name, RouterInterface $router, CacheInterface $cacheDriver = null, Translate $translate = null)
{
$this->router = $router;
parent::__construct($cacheDriver, $translate);
}
...
}
Позволяет создать объект необходимого класса с автоматическим определением и инъекцией зависимостей:
$myCommandObject = $container->construct(\My\MyCommand::class);
Также вторым входящим параметром можно передать агрументы конструктора:
$myCommandObject = $container->construct(\My\MyCommand::class, ['router' => $someAnotherRouter]);
Позволяет вызвать callable (метод объекта, анонимную функцию) с автоматическим определением и инъекцией зависимостей:
$container->invoke([$myCommandObject, 'handle']);
Также вторым входящим параметром можно передать агрументы callable:
$container->invoke([$myCommandObject, 'handle'], ['args' => [1,2]]);