Symfony: Pozor na služby závislé na EntityManageru!

PHP

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 :)

Komentáře