Symfony-docs: Doctrine entity listeners

Created on 19 Feb 2015  路  17Comments  路  Source: symfony/symfony-docs

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.

Doctrine actionable good first 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: 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

All 17 comments

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

Was this page helpful?
0 / 5 - 0 ratings