⬆️ ⬇️

Dependency injection in a Doctrine entity using the Symfony Dependency Injection Component

Although dependency injection is essentially considered a bad practice in terms of DDD, there are situations in which this is very convenient. The legitimacy of using such an approach, but exactly like comparing it with alternatives (double dispatching, events), is not the topic of this article. I want to talk about the technical implementation - about the integration of the Symfony Dependency Injection Component (hereinafter referred to as DIC) with Doctrine to automatically embed dependencies in loadable entities. The versions of symfony and doctrine used are 2. *.





So, we have an entity in which you need to embed dependencies when loading or when creating:



<?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; } } 


')

Unfortunately, I did not find a simple way to implement dependency injection into the constructor when using Doctrine - the creation of an object is located deep in the depths of the Unit of Work and ClassMetadata. Therefore, the implementation will be carried out using setters.



Doctrine events and DIC tags will be used for integration. The format of the DIC configs is yaml, but you can use your favorite one.



We want dependencies to be embedded into it after the entity is loaded. For this we have a postLoad event at our disposal.



Implement EventSubscriber, which responds to this event:



 <?php namespace Persistence; use Doctrine\ORM\Events; use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\Common\EventSubscriber; use DependencyInjection\Injector; 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); } } 




And connect it to the entity manager after creating



 $entityManager->getEventManager()->addEventSubscriber($entityConfigurator); 




All implementation work will occur within the Injector class.



The dependency injection algorithm is simple:





Implementation:

 <?php namespace DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; class Injector { const DOCTRINE_ENTITY_TAG = 'doctrine-entity'; /** *@var \Symfony\Component\DependencyInjection\ContainerBuilder */ 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)); } } } 


If necessary, the constant DOCTRINE_ENTITY_TAG can be replaced by an array of different tags.



In order for Injector to be able to inject dependencies, entities need to be configured in the DIC (yaml) configuration:



 services:
    ... ....
    entity-object-title:
         class: 'Domain \ SomeEntity'
         tags: [{name: "doctrine-entity"}]
         abstract: true
         calls:
           - [setSomeService, [@ some-service]]
           - [setSomeService2, [@ some-service2]]
    ... ..








Now when loading an entity using Doctrine, dependencies will be automatically injected.



If you need to implement dependency injection for new objects, you can use the Injector inside the relevant factories or factory methods.



Ps. In the Injector class, ContainerBuilder is used directly for simplicity, but in a real project, a wrapper over it is used. This allows you to encapsulate the features of a symfony DIC and, if necessary, use other DI libraries. More importantly, it allows you to use the principle of “Don't mock types you don't have”.

Source: https://habr.com/ru/post/173275/



All Articles