Несмотря на то, что внедрение зависимостей в сущности считается плохой практикой с точки зрения DDD, существуют ситуации в которых это очень удобно. Правомерность использования такого подхода, а ровно как и сравнение его с альтернативами (двойная диспетчеризация, события) не является темой данной статьи. Я хочу рассказать о технической реализации — об интеграции Symfony Dependency Injection Component (далее DIC) с Doctrine для автоматического внедрения зависимостей в загружаемые сущности. Используемые версии Symfony и Doctrine — 2.*.
Итак, у нас есть сущность в которую необходимо внедрять зависимости при загрузке, либо при создании:
<?php
namespace Domain;
/**
* @Entity
*/
class SomeEntity
{
…...
private $someService;
private $anotherService;
…...
public function setSomeService(SomeService $someService)
{
$this->someService = $someService;
}
public function setAnotherService2(AnotherService $anotherService)
{
$this->anotherService = $anotherService;
}
}
К сожалению, я не нашёл простого способа реализовать внедрение зависимостей в конструктор при использовании Doctrine — создание объекта находится глубоко в недрах Unit of Work и ClassMetadata. Поэтому внедрение будет осуществляться при помощи сеттеров.
Для интеграции будет использоваться события Doctrine и теги DIC. Формат конфигов DIC — yaml, но можете использовать свой любимый.
Мы хотим, чтобы после загрузки сущности в неё внедрялись зависимости. Для этого в нашем распоряжении есть событие postLoad.
Реализуем EventSubscriber, который реагирует на это событие:
<?php
namespace Persistence;
use DoctrineORMEvents;
use DoctrineORMEventLifecycleEventArgs;
use DoctrineCommonEventSubscriber;
use DependencyInjectionInjector;
class EntityConfigurator implements EventSubscriber
{
private $injector;
public function __construct(Injector $injector)
{
$this->injector = $injector;;
}
public function getSubscribedEvents()
{
return [Events::postLoad];
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$this->injector->injectSevicesTo($entity);
}
}
И подключим его к entity manager после создания
$entityManager->getEventManager()->addEventSubscriber($entityConfigurator);
Вся работа по внедрению будет происходить внутри класса Injector.
Алгоритм внедрения зависимостей прост:
- получить все сервисы, помеченные специальным тегом из DIC
- при загрузке, если какой-либо из полученных сервисов имеет класс, равный классу загружаемой сущности — выполнить все сеттер вызовы
Реализация:
<?php
namespace DependencyInjection;
use SymfonyComponentDependencyInjectionContainerBuilder;
class Injector
{
const DOCTRINE_ENTITY_TAG = 'doctrine-entity';
/**
*@var SymfonyComponentDependencyInjectionContainerBuilder
*/
private $container;
private $configurableClasses = [];
public function __construct(ContainerBuilder $container)
{
$this->container = $container;
$this->prepareConfigurableClasses();
}
private function prepareConfigurableClasses()
{
// Получаем список сервисов с нужным тэгом
foreach($this->container->findTaggedServiceIds(self::DOCTRINE_ENTITY_TAG) as $id => $tag) {
// Получаем определение сервиса
$definition = $this->container->findDefinition($id);
// Добавляем все нужные setter вызовы
$this->configurableClasses[$definition->getClass()] = $definition->getMethodCalls();
}
}
public function injectSevicesTo($object)
{
if(!is_object($object) || !array_key_exists(get_class($object), $this->configurableClasses)) {
return;
}
// Нужно для подстановки параметром в конфигах DIC
$parameter_bag = $this->container->getParameterBag();
$calls = $this->configurableClasses[get_class($object)];
foreach($calls as $call) {
//Собственно вставка зависимостей
$parametrized_references = $parameter_bag->resolveValue($call[1]);
call_user_func_array(array($object, $call[0]), $this->container->resolveServices($parametrized_references));
}
}
}
При необходимости константу DOCTRINE_ENTITY_TAG можно заменить массивом различных тегов.
Для того, чтобы Injector мог внедрять зависимости, сущности необходимо прописать в конфигурации DIC (yaml):
services: ….... entity-object-title: class: 'DomainSomeEntity’ tags: [{ name: "doctrine-entity" }] abstract: true calls: - [ setSomeService, [ @some-service ] ] - [ setSomeService2, [ @some-service2] ] …..
entity-object-title
— произвольное имя, используется в описательных целях.abstract: true
— отключаем возможность создания сервиса напрямую. Можно для этих целей использоватьpublic: false
.calls
— перечисление сеттеров в которых происходит внедрение зависимостей.
Теперь при загрузке сущности при помощи Doctrine будет происходить автоматическое внедрение зависимостей.
Если нужно осуществлять внедрение зависимостей для новых объектов, можно использовать Injector внутри соответствующих фабрик или фабричных методов.
PS. В классе Injector для простоты напрямую используется ContainerBuilder, но в реальном проекте используется обёртка над ним. Это позволяет инкапсулировать особенности Symfony DIC и, при необходимости, использовать другие DI библиотеки. Что ещё более важно, это позволяет использовать принцип “Don’t mock types you don’t have”.
Автор: kirksa