5.4 Способы внедрения зависимостей (Dependency Injection)
Есть 2 способа внедрения зависимостей:
- Типичный способ удостовериться в том, что сервис получил свои зависимости - это прописать зависимости в качестве аргументов конструктора.
class SomeService { private $container; public function __construct(ContainerInterface $container) { $this->container = $container; } }
В некоторых случаях требуется не переопределять конструктор или не добавлять к нему дополнительные обязательные аргументы. Или какие-либо зависимости еще не определены в момент создания сервиса. В этих случаях можно добавить в класс сервиса метод-сеттер, позволив таким образом внедрять зависимость сразу после создания сервиса. Но в таком случае необходимо позаботиться, чтобы сеттер был обязательно вызван.
class SomeService { private $container; public function setContainer(ContainerInterface $storage) { $this->container = $container; } public function getContainer() { return $this->container; } }
Преимущество использования сеттеров в том, что не требуется иметь аргумент в конструкторе для каждой зависимости. Иногда это означает что нет необходимости создавать конструктор вообще, или что можно оставить текущий конструктор без изменений.
Единственный недостаток данного подхода в том, что можно забыть вызвать сеттер и получить ошибку из-за отсутствия необходимой зависимости. Для избежания данной ошибки можно обернуть вызовы зависимостей проверкой на их существование.
...
public function getContainer()
{
if (!($this->container instanceof ContainerInterface)) {
throw new \RuntimeException(‘Service container is missing’);
}
return $this->container;
}
...
Компонент Symfony Dependency Injection включает в себя интерфейс ContainerAwareInterface и трейт ContainerAwareTrait (\Symfony\Component\DependencyInjection\ContainerAwareTrait), который уже содержит приватное свойство $container и его сеттер. Классы, в которые передаётся трейт ContainerAwareTrait автоматически получают контейнер через сеттер setContainer(). Стандартный класс Controller из бандла FrameworkBundle использует как раз ContainerAwareTrait.
Однако такой подход вызовет большое количество дублирующего кода в конфигурациях сервиса. Дабы избежать это, можно создать абстракный сервис, в который добавить вызов setContainer(). И уже этот сервис наследовать всеми остальными.
abstract_container_aware:
abstract: true calls: - [setContainer, ['@service_container]]
some_service:
class: AcmeBundle\Service\SomeService
parent: abstract_container_aware
Родительский сервис не обязательно делать абстрактным. Если он будет использоваться напрямую - можно опустить параметр abstract.