As mentioned at the end of Creating the Listener Class paragraph from event listeners and subscribers doctrine cookbook, entity listeners are introduced in Doctrine 2.4.
An entity listener is a lifecycle listener class used for a specific entity. Entity listeners can be defined as services and the great thing is that they can have container services/parameters injected. Shouldn't this be documented? For example:
services:
user_listener:
class: \UserListener
arguments:
- "%some_param%"
tags:
- { name: doctrine.orm.entity_listener }
- { name: doctrine.orm.entity_listener, entity_manager: custom }
Entity listeners are barely mentioned here. This was ticket was opened as a suggestion.
:+1: on changing the note in a new section that shows a very minimal example of an entity listener.
Please note that every listener is a service and can have parameters injected.
@lucascherifi Is this snippet actually working? I doesn't seem to work for me. I tried to debug this for few hours and find out that listener wont be attached, until there are two tag attributes present: entity and event. The logic is in \Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\EntityListenerPass::process() function.
There is a condition:
if (isset($attributes['entity']) && isset($attributes['event'])) {
$this->attachToListener($container, $name, $id, $attributes);
}
So correct minimal example should be something similar to this:
app.event_listener.post:
class: AppBundle\Entity\Listener\PostListener
arguments: []
tags:
- { name: doctrine.orm.entity_listener, entity: AppBundle\Entity\Post, event: prePersist }
- { name: doctrine.orm.entity_listener, entity: AppBundle\Entity\Post, event: preUpdate }
I'm using:
doctrine/annotations v1.2.7
doctrine/cache v1.6.0
doctrine/collections v1.3.0
doctrine/common v2.6.1
doctrine/data-fixtures v1.1.1
doctrine/dbal v2.5.4
doctrine/doctrine-bundle 1.6.2
doctrine/doctrine-cache-bundle 1.3.0
doctrine/doctrine-fixtures-bundle 2.3.0
doctrine/inflector v1.1.0
doctrine/instantiator 1.0.5
doctrine/lexer v1.0.1
doctrine/orm v2.5.4
symfony/symfony v2.8.2
OK, so I figured it out. If I don't want to use full specification like:
- { name: doctrine.orm.entity_listener, entity: AppBundle\Entity\Post, event: prePersist }
I need to annotate my entity with something like this:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository")
* @ORM\EntityListeners({"AppBundle\Entity\Listener\PostListener"})
*/
class Post
{
//....
}
Then in services.yml I can just use short version:
app.event_listener.post:
class: AppBundle\Entity\Listener\PostListener
arguments: []
tags:
- { name: doctrine.orm.entity_listener }
My AppBundle\Entity\Listener\PostListener:
<?php
namespace AppBundle\Entity\Listener;
use AppBundle\Entity\Post;
use Doctrine\ORM\Event\LifecycleEventArgs;
class PostListener
{
public function preUpdate(Post $user, LifecycleEventArgs $event)
{
die('preUpdate');
}
public function prePersist(Post $user, LifecycleEventArgs $event)
{
die('prePersist');
}
}
It seems that Symfony / doctrine is using its "magic" and searching for services with tag doctrine.orm.entity_manager and class defined in annotation.
How did you do that?
@ORM\EntityListeners({"AppBundle\Entity\Listener\PostListener"})
is not working for me. I'm using
"symfony/symfony": "2.8.*",
"doctrine/orm": "2.5.*",
"doctrine/doctrine-bundle": "~1.4",
@miholeus Did you set listener service with correct tag? Do you use annotations or yaml? Do long version
app.event_listener.post:
class: AppBundle\Entity\Listener\PostListener
arguments: []
tags:
- { name: doctrine.orm.entity_listener, entity: AppBundle\Entity\Post, event: prePersist }
- { name: doctrine.orm.entity_listener, entity: AppBundle\Entity\Post, event: preUpdate }
work?
Do you have prePersist() and preUpdate() methods inside listener?
Do you have doctrine/annotations installed?
@piotrekkr Yes, "long version" is working fine. But the short version
app.event_listener.post:
class: AppBundle\Entity\Listener\PostListener
arguments: []
tags:
- { name: doctrine.orm.entity_listener }
doesn't work. I have both prePersist() and preUpdate methods. They are not invoked inspite of using
@ORM\EntityListeners annotation in entity.
Other annotations in entity work fine.
@miholeus do you have XML or YAML mapping files ? If yes, the ORM simply never reads the annotations, as DoctrineBundle will configure your bundle to use XML or YAML. In such case, you need to edit your XML/YAML file, not the annotation (and I would advice to remove the annotation entirely if your bundle does not rely on them, to avoid WTFs about why changes in them are not taken into account)
@stof yes, you are right. I used YAML mapping file. That was the reason. I've checked on new bundle and everything went smoothly. I've figured out that in my case I should append following lines to make it work:
entityListeners:
TaskBundle\Entity\Listener\PostListener: ~
Thanks for pointing the right direction)
Though my listeners work fine I need to inject the MongoDB ODM Document Manager. Please see:
https://gist.github.com/webdevilopers/e65f795749c40601276f60a5a3517a85#file-customerlistener-php
Unfortunately adding the <tag name="doctrine.orm.entity_listener" /> causes an circular reference:
Is something wrong with my XML service definition? Or is this caused by the Document Manager and I need to inject the full Container for instance?
I tried injecting the container but get another circular reference:
https://gist.github.com/webdevilopers/e65f795749c40601276f60a5a3517a85#file-customerlistener-php
If I remove the tag the service is not injected an the constructor is missing the first argument of course.
This approach seemed to work for others though:
http://intelligentbee.com/blog/2015/02/18/symfony2-doctrine-2-entity-listeners/
Looks like Composer was using "doctrine/doctrine-bundle": "~1.2". I will try 1.3.. Bet it works!
Still getting it with 1.6:
Circular reference detected for service "doctrine.orm.default_entity_manager", path: "cache_warmer -> twig -> security.authorization_checker -> security.authentication.manager -> fos_user.user_manager -> fos_user.entity_manager -> doctrine.orm.default_entity_manager -> doctrine.orm.default_entity_listener_resolver -> customer.listener -> doctrine_mongodb.odm.default_document_manager -> doctrine_mongodb.odm.default_connection -> doctrine_mongodb.odm.event_manager -> gedmo.listener.reference.mongodb".
Maybe there is a problem with other listeners like Gedmo?
Indeed there was an entry inside config/services.yml:
# gedmo.listener.reference.mongodb:
# class: Gedmo\References\ReferencesListener
# tags:
# - { name: doctrine_mongodb.odm.event_subscriber }
# calls:
# - [ setAnnotationReader, [ "@annotation_reader" ] ]
# - [ registerManager, [ 'entity', "@doctrine.orm.entity_manager" ] ]
Unfortunately it is used by the application at a different place. Any idea how this was causing the circular reference @gedmo @Atlantic18?
@webdevilopers I got circular reference too, looking why and how to fix this
I just found this issue through google, while looking for if there is any kind of integration between doctrine's entity listeners, and symfony's DI container, turns out there is.
I can confirm, it's working for me.
About circular references you should have a look at the pending PR https://github.com/symfony/symfony-docs/pull/8080.
I just expanded https://symfony.com/doc/current/bundles/DoctrineBundle/entity-listeners.html to show a complete example. And I changed the link which @luciantugui mentioned in his original post to lead to that page - here's the PR: https://github.com/symfony/symfony-docs/pull/10195
So please merge that PR, then you can close this issue :-)
Most helpful comment
@lucascherifi Is this snippet actually working? I doesn't seem to work for me. I tried to debug this for few hours and find out that listener wont be attached, until there are two tag attributes present:
entityandevent. The logic is in\Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\EntityListenerPass::process()function.There is a condition:
So correct minimal example should be something similar to this:
I'm using: