POZOR! Článek jsem napsal před více jak rokem, a tudíž už nemusí reflektovat můj nynější názor nebo může být zastaralý.
Abych to nejprve uvedl na pravou míru: Služby, které mají závislost (ať už v constructoru nebo setteru) na EntityManagerInterface nejsou sami o sobě problém. Ale jen do chvíle, než se takové služby začnou využívat v Doctrine listenerech a subscriberech. Pak se dějou věci.
Uvedu příklad (celý kód najdete na github.com/MichalKatuscak/symfony-problem-with-services).
(Autorem kódu a spoluautorem článku je můj kolega Libor Matásek, tímto mu děkuji za poskytnutí podkladů :))
Mám controller a službu:
/src/Controller/TestController.php
<?php
namespace App\Controller;
use App\Entity\Test;
use App\Service\SyncJobFulltext;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class TestController extends AbstractController
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* @Route("/test-repo")
* @return Response
*/
public function testRepo()
{
$testRepo = $this->entityManager->getRepository(Test::class);
$testEntity = $testRepo->find(1);
$testEntity->setName('hello world');
$this->entityManager->persist($testEntity);
// funguje dobře
$this->entityManager->flush();
return new Response('test repo');
}
/**
* @Route("/test-repo-inside-service")
* @param SyncJobFulltext $service
* @return Response
*/
public function testRepoInsideService(SyncJobFulltext $service)
{
// v metodě "sync" je stejný kód jako ve zdejší "testRepo"
$service->sync();
return new Response('test repo inside service');
}
}
/src/Service/SyncJobFulltext.php
<?php
namespace App\Service;
use App\Entity\Test;
use Doctrine\ORM\EntityManagerInterface;
class SyncJobFulltext
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function sync()
{
$testRepo = $this->entityManager->getRepository(Test::class);
$testEntity = $testRepo->find(1);
$testEntity->setName(rand());
$this->entityManager->persist($testEntity);
$this->entityManager->flush();
}
}
Až do teď je vše v pořádku. Vše funguje. A vy si myslíte, jak je vše skvělé. Ale není. Jednou se vývojář (a nemusíte to být zrovna vy) rozhodne, že službu „SyncJobFulltext“ použije ve svém Doctriner EventSubscriberu:
/src/EventSubscriber/TestSubscriber.php
<?php
namespace App\EventSubscriber;
use App\Service\SyncJobFulltext;
use Doctrine\Common\EventSubscriber;
class TestSubscriber implements EventSubscriber
{
public function __construct(SyncJobFulltext $syncJobFulltext)
{
}
public function getSubscribedEvents()
{
return [];
}
}
/config/services.yml
services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
App\EventSubscriber\TestSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
Najednou se v databázi začnou vytvářet nové položky, ačkoli nikde žádnou novou entitu nevytváříte. Jak to? Jaktože stejná služba se chová najednou jinak?
Problém je, že „TestSubscriber“ je závislý na službě „SyncJobFulltext“ a ta je závislá na „EntityManager“ a ten je závislý na „EventManager“ a ten zpracovává EventSubscribery. Zde už začíná být trochu jasné, že může dojít smyčce. Zjednodušeně: Aby se s tím Symfony vyrovnalo, vytvoří ještě jeden DI container a to je ten problém. Nebudete na to upozorněni. V každém containeru je instance EntityManageru a jedna z nich si uchovává informaci o tom, že upravuje entitu (ta v controlleru), ale ta druhá ne (ta ve službě).
Řešením je vytahovat EntityManager z každého eventu a předávat ho službám přímo (služba jako takové tedy nesmí být přímo na EntityManageru závislá pokud ji chcete mít v DI containeru):
getEntityManager();
$entity = $args->getEntity();
$this->someService->doingSomething($em, $entity);
}
//...
Znáte někoho, komu by článek mohl pomoct? Zasdílejte mu ho :)