Skip to content

Possible BC break introduce since v3.4.20 #30017

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
nykopol opened this issue Jan 29, 2019 · 7 comments
Closed

Possible BC break introduce since v3.4.20 #30017

nykopol opened this issue Jan 29, 2019 · 7 comments

Comments

@nykopol
Copy link
Contributor

nykopol commented Jan 29, 2019

Symfony version(s) affected: v3.4.20 and v3.4.21

Description
I'm not sure if it's a BC break in Symfony or a bad practice in Doctrine ORM (doctrine/orm:v2.5.14).
Since the commit 0d0be12, if a service is attached to the lifecycle events of doctrine, the service is not called. Instead, doctrine try to implement a new instance of the service (without the arguments).

How to reproduce

Configure a service that is attached to a doctrine event:

services:
  app.my_service:
    class: App\Listener\MyService
    arguments: ['foo', 'bar']
    tags:
      - { name: doctrine.orm.entity_listener, entity: App\Entity\MyEntity, event: preUpdate }

The service:

namespace App\Listener;

class MyService
{
  public function __construct($foo, $bar) { }
  //...
}

Trigger the event and an error occur:

Type error: Too few arguments to function App\Listener\MyServicer::__construct(), 0 passed in /.../vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/DefaultEntityListenerResolver.php on line 74 and exactly 2 expected

Additional context
By digging inside, I found that inside the ListenersInvoker of Doctrine, a resolver has the responsibility to resolve the class and method attached to an event into an instance.

Before v3.4.20, the resolver was an instance of ContainerAwareEntityListenerResolver which is capable of translating the class in the corresponding service.
Since v3.4.20, it's a DefaultEntityResolverListener which only create a new instance of the class without arguments.

It's a problem of timing.

The resolver is not directly injected on the constructor of the ListenersInvoker. Instead, the ListenersInvoker receive the doctrine.orm.configuration from which it retrieve the resolver inside the constructor. But at this state, the doctrine.orm.configuration is not yet configured.

The doctrine.orm.configuration service is configured by some setters. The fact is that since the v3.4.20, the setters are called after ListenersInvoker::construct(). So the ListenersInvoker don't have the good resolver.

@nicolas-grekas
Copy link
Member

Would you be able to submit a failing test case?
Or maybe a small reproducer app?

@nykopol
Copy link
Contributor Author

nykopol commented Jan 29, 2019

@nicolas-grekas Yes. Here is a broken app : https://github.com/nykopol/broken-app
You just have to go to the default controller.
I noticed that this only occur if the listener depends on doctrine.orm.entity_manager.

@vudaltsov
Copy link
Contributor

vudaltsov commented Jan 29, 2019

@nykopol , you should not inject entity manager via constructor because that leads to a circular dependency. Listeners are "child" objects in relation to the manager, so they cannot receive it as a dependency.

Instead you should use the second argument of your callable to retrieve the relevant entity manager:

<?php

namespace AppBundle\Listener;

use AppBundle\Entity\MyEntity;
use Doctrine\ORM\Event\PreUpdateEventArgs;

class ServiceThatWillNotFailAnymore
{
    public function __construct(/* other args */)
    {
    }

    public function prePersist(MyEntity $entity, PreUpdateEventArgs $args)
    {
        $entityManager = $args->getEntityManager();
    }
}

@vudaltsov
Copy link
Contributor

I hope, that will solve your issue.

Btw, you can find more about the listener's arguments in the official Doctrine Documentation.

I think we should explain how to retrieve entity manager in the Doctrine Bundle's docs.

@javiereguiluz, @nicolas-grekas, WDYT?

@nykopol
Copy link
Contributor Author

nykopol commented Jan 29, 2019

@vudaltsov I understand the fact that Listener is a "child" object and that I can get the manager in the prePersist. But in real life, my Listener doesn't inject directly doctrine.orm.entity_manager but an other service which inject it.

For the principle, a Listener is a service. Why he's not allowed to inject this service ? We can imagine a use case where the listener collect some information on prePersist and the persist something at kernel.finish_request. It would require the entity manager.

@vudaltsov
Copy link
Contributor

Circular dependency is a bad practice 99% of the time, no matter how long the dependency chain is. As a rule it points at some architectural problems.

In your case I see the problem of mixing different contexts. The context of the entity manager transaction and the request context. Here you can create a separate service called SomeCollection with add(Type $item): void and all(): iterable|Type[] methods. It will be responsible for collecting and providing information in different contexts: you will inject SomeCollection into your SomeEntityPrePersistListener (and probably some other services) for collecting info and into OnFinishRequestListener for actually using it. OnFinishRequestListener will also inject entity manager to do entity-related stuff.

While my solution is still not ideal and might be improved for your particular case, it uses a mediator object (see Indirection in GRASP) to organize communication between services and to eliminate the circular dependency problem.

nicolas-grekas added a commit that referenced this issue Jan 30, 2019
…kas)

This PR was merged into the 3.4 branch.

Discussion
----------

[DI] Fix dumping Doctrine-like service graphs

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #30017 #29637 #29693
| License       | MIT
| Doc PR        | -

I'm unable to provide a reproducer for this, the required service reference graph is too crazy, but that does the job :)

Commits
-------

ed96830 [DI] Fix dumping Doctrine-like service graphs
@nicolas-grekas
Copy link
Member

thanks for the help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants