Doctrine Events

Doctrine Events

Doctrine, the set of PHP libraries used by Symfony to work with databases, provides a lightweight event system to update entities during the application execution. These events, called lifecycle events, allow to perform tasks such as “update the createdAt property automatically right before persisting entities of this type”.

Doctrine triggers events before/after performing the most common entity operations (e.g. prePersist/postPersist, preUpdate/postUpdate) and also on other common tasks (e.g. loadClassMetadata, onClear).

There are different ways to listen to these Doctrine events:

  • Lifecycle callbacks, they are defined as public methods on the entity classes and they are called when the events are triggered;
  • Lifecycle listeners and subscribers, they are classes with callback methods for one or more events and they are called for all entities;
  • Entity listeners, they are similar to lifecycle listeners, but they are called only for the entities of a certain class.

These are the drawbacks and advantages of each one:

  • Callbacks have better performance because they only apply to a single entity class, but you can’t reuse the logic for different entities and they don’t have access to Symfony services;
  • Lifecycle listeners and subscribers can reuse logic among different entities and can access Symfony services but their performance is worse because they are called for all entities;
  • Entity listeners have the same advantages of lifecycle listeners and they have better performance because they only apply to a single entity class.

This article only explains the basics about Doctrine events when using them inside a Symfony application. Read the official docs about Doctrine events to learn everything about them.

See also

This article covers listeners and subscribers for Doctrine ORM. If you are using ODM for MongoDB, read the DoctrineMongoDBBundle documentation.

Doctrine Lifecycle Callbacks

Lifecycle callbacks are defined as public methods inside the entity you want to modify. For example, suppose you want to set a createdAt date column to the current date, but only when the entity is first persisted (i.e. inserted). To do so, define a callback for the prePersist Doctrine event:

  • Annotations

    1. // src/Entity/Product.php
    2. namespace App\Entity;
    3. use Doctrine\ORM\Mapping as ORM;
    4. // When using annotations, don't forget to add @ORM\HasLifecycleCallbacks()
    5. // to the class of the entity where you define the callback
    6. /**
    7. * @ORM\Entity()
    8. * @ORM\HasLifecycleCallbacks()
    9. */
    10. class Product
    11. {
    12. // ...
    13. /**
    14. * @ORM\PrePersist
    15. */
    16. public function setCreatedAtValue(): void
    17. {
    18. $this->createdAt = new \DateTimeImmutable();
    19. }
    20. }
  • YAML

    1. # config/doctrine/Product.orm.yml
    2. App\Entity\Product:
    3. type: entity
    4. # ...
    5. lifecycleCallbacks:
    6. prePersist: ['setCreatedAtValue']
  • XML

    1. <!-- config/doctrine/Product.orm.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
    6. https://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    7. <entity name="App\Entity\Product">
    8. <!-- ... -->
    9. <lifecycle-callbacks>
    10. <lifecycle-callback type="prePersist" method="setCreatedAtValue"/>
    11. </lifecycle-callbacks>
    12. </entity>
    13. </doctrine-mapping>

Note

Some lifecycle callbacks receive an argument that provides access to useful information such as the current entity manager (e.g. the preUpdate callback receives a PreUpdateEventArgs $event argument).

Doctrine Lifecycle Listeners

Lifecycle listeners are defined as PHP classes that listen to a single Doctrine event on all the application entities. For example, suppose that you want to update some search index whenever a new entity is persisted in the database. To do so, define a listener for the postPersist Doctrine event:

  1. // src/EventListener/SearchIndexer.php
  2. namespace App\EventListener;
  3. use App\Entity\Product;
  4. use Doctrine\Persistence\Event\LifecycleEventArgs;
  5. class SearchIndexer
  6. {
  7. // the listener methods receive an argument which gives you access to
  8. // both the entity object of the event and the entity manager itself
  9. public function postPersist(LifecycleEventArgs $args): void
  10. {
  11. $entity = $args->getObject();
  12. // if this listener only applies to certain entity types,
  13. // add some code to check the entity type as early as possible
  14. if (!$entity instanceof Product) {
  15. return;
  16. }
  17. $entityManager = $args->getObjectManager();
  18. // ... do something with the Product entity
  19. }
  20. }

The next step is to enable the Doctrine listener in the Symfony application by creating a new service for it and tagging it with the doctrine.event_listener tag:

  • YAML

    1. # config/services.yaml
    2. services:
    3. # ...
    4. App\EventListener\SearchIndexer:
    5. tags:
    6. -
    7. name: 'doctrine.event_listener'
    8. # this is the only required option for the lifecycle listener tag
    9. event: 'postPersist'
    10. # listeners can define their priority in case multiple listeners are associated
    11. # to the same event (default priority = 0; higher numbers = listener is run earlier)
    12. priority: 500
    13. # you can also restrict listeners to a specific Doctrine connection
    14. connection: 'default'
  • XML

    1. <!-- config/services.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
    5. <services>
    6. <!-- ... -->
    7. <!--
    8. * 'event' is the only required option that defines the lifecycle listener
    9. * 'priority': used when multiple listeners are associated to the same event
    10. * (default priority = 0; higher numbers = listener is run earlier)
    11. * 'connection': restricts the listener to a specific Doctrine connection
    12. -->
    13. <service id="App\EventListener\SearchIndexer">
    14. <tag name="doctrine.event_listener"
    15. event="postPersist"
    16. priority="500"
    17. connection="default"/>
    18. </service>
    19. </services>
    20. </container>
  • PHP

    1. // config/services.php
    2. use App\EventListener\SearchIndexer;
    3. // listeners are applied by default to all Doctrine connections
    4. $container->autowire(SearchIndexer::class)
    5. ->addTag('doctrine.event_listener', [
    6. // this is the only required option for the lifecycle listener tag
    7. 'event' => 'postPersist',
    8. // listeners can define their priority in case multiple listeners are associated
    9. // to the same event (default priority = 0; higher numbers = listener is run earlier)
    10. 'priority' => 500,
    11. # you can also restrict listeners to a specific Doctrine connection
    12. 'connection' => 'default',
    13. ])
    14. ;

Tip

Symfony loads (and instantiates) Doctrine listeners only when the related Doctrine event is actually fired; whereas Doctrine subscribers are always loaded (and instantiated) by Symfony, making them less performant.

Doctrine Entity Listeners

Entity listeners are defined as PHP classes that listen to a single Doctrine event on a single entity class. For example, suppose that you want to send some notifications whenever a User entity is modified in the database. To do so, define a listener for the postUpdate Doctrine event:

  1. // src/EventListener/UserChangedNotifier.php
  2. namespace App\EventListener;
  3. use App\Entity\User;
  4. use Doctrine\Persistence\Event\LifecycleEventArgs;
  5. class UserChangedNotifier
  6. {
  7. // the entity listener methods receive two arguments:
  8. // the entity instance and the lifecycle event
  9. public function postUpdate(User $user, LifecycleEventArgs $event): void
  10. {
  11. // ... do something to notify the changes
  12. }
  13. }

The next step is to enable the Doctrine listener in the Symfony application by creating a new service for it and tagging it with the doctrine.orm.entity_listener tag:

  • YAML

    1. # config/services.yaml
    2. services:
    3. # ...
    4. App\EventListener\UserChangedNotifier:
    5. tags:
    6. -
    7. # these are the options required to define the entity listener
    8. name: 'doctrine.orm.entity_listener'
    9. event: 'postUpdate'
    10. entity: 'App\Entity\User'
    11. # these are other options that you may define if needed
    12. # set the 'lazy' option to TRUE to only instantiate listeners when they are used
    13. # lazy: true
    14. # set the 'entity_manager' option if the listener is not associated to the default manager
    15. # entity_manager: 'custom'
    16. # by default, Symfony looks for a method called after the event (e.g. postUpdate())
    17. # if it doesn't exist, it tries to execute the '__invoke()' method, but you can
    18. # configure a custom method name with the 'method' option
    19. # method: 'checkUserChanges'
  • XML

    1. <!-- config/services.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
    5. <services>
    6. <!-- ... -->
    7. <service id="App\EventListener\UserChangedNotifier">
    8. <!--
    9. * These are the options required to define the entity listener:
    10. * * name
    11. * * event
    12. * * entity
    13. *
    14. * These are other options that you may define if needed:
    15. * * lazy: if TRUE, listeners are only instantiated when they are used
    16. * * entity_manager: define it if the listener is not associated to the default manager
    17. * * method: by default, Symfony looks for a method called after the event (e.g. postUpdate())
    18. * if it doesn't exist, it tries to execute the '__invoke()' method, but
    19. * you can configure a custom method name with the 'method' option
    20. -->
    21. <tag name="doctrine.orm.entity_listener"
    22. event="postUpdate"
    23. entity="App\Entity\User"
    24. lazy="true"
    25. entity_manager="custom"
    26. method="checkUserChanges"/>
    27. </service>
    28. </services>
    29. </container>
  • PHP

    1. // config/services.php
    2. use App\Entity\User;
    3. use App\EventListener\UserChangedNotifier;
    4. $container->autowire(UserChangedNotifier::class)
    5. ->addTag('doctrine.orm.entity_listener', [
    6. // These are the options required to define the entity listener:
    7. 'event' => 'postUpdate',
    8. 'entity' => User::class,
    9. // These are other options that you may define if needed:
    10. // set the 'lazy' option to TRUE to only instantiate listeners when they are used
    11. // 'lazy' => true,
    12. // set the 'entity_manager' option if the listener is not associated to the default manager
    13. // 'entity_manager' => 'custom',
    14. // by default, Symfony looks for a method called after the event (e.g. postUpdate())
    15. // if it doesn't exist, it tries to execute the '__invoke()' method, but you can
    16. // configure a custom method name with the 'method' option
    17. // 'method' => 'checkUserChanges',
    18. ])
    19. ;

New in version 4.4: Support for invokable listeners (using the __invoke() method) was introduced in Symfony 4.4.

Doctrine Lifecycle Subscribers

Lifecycle subscribers are defined as PHP classes that implement the Doctrine\Common\EventSubscriber interface and which listen to one or more Doctrine events on all the application entities. For example, suppose that you want to log all the database activity. To do so, define a subscriber for the postPersist, postRemove and postUpdate Doctrine events:

  1. // src/EventListener/DatabaseActivitySubscriber.php
  2. namespace App\EventListener;
  3. use App\Entity\Product;
  4. use Doctrine\Common\EventSubscriber;
  5. use Doctrine\ORM\Events;
  6. use Doctrine\Persistence\Event\LifecycleEventArgs;
  7. class DatabaseActivitySubscriber implements EventSubscriber
  8. {
  9. // this method can only return the event names; you cannot define a
  10. // custom method name to execute when each event triggers
  11. public function getSubscribedEvents(): array
  12. {
  13. return [
  14. Events::postPersist,
  15. Events::postRemove,
  16. Events::postUpdate,
  17. ];
  18. }
  19. // callback methods must be called exactly like the events they listen to;
  20. // they receive an argument of type LifecycleEventArgs, which gives you access
  21. // to both the entity object of the event and the entity manager itself
  22. public function postPersist(LifecycleEventArgs $args): void
  23. {
  24. $this->logActivity('persist', $args);
  25. }
  26. public function postRemove(LifecycleEventArgs $args): void
  27. {
  28. $this->logActivity('remove', $args);
  29. }
  30. public function postUpdate(LifecycleEventArgs $args): void
  31. {
  32. $this->logActivity('update', $args);
  33. }
  34. private function logActivity(string $action, LifecycleEventArgs $args): void
  35. {
  36. $entity = $args->getObject();
  37. // if this subscriber only applies to certain entity types,
  38. // add some code to check the entity type as early as possible
  39. if (!$entity instanceof Product) {
  40. return;
  41. }
  42. // ... get the entity information and log it somehow
  43. }
  44. }

The next step is to enable the Doctrine subscriber in the Symfony application by creating a new service for it and tagging it with the doctrine.event_subscriber tag:

  • YAML

    1. # config/services.yaml
    2. services:
    3. # ...
    4. App\EventListener\DatabaseActivitySubscriber:
    5. tags:
    6. - { name: 'doctrine.event_subscriber' }
  • XML

    1. <!-- config/services.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
    5. <services>
    6. <!-- ... -->
    7. <service id="App\EventListener\DatabaseActivitySubscriber">
    8. <tag name="doctrine.event_subscriber"/>
    9. </service>
    10. </services>
    11. </container>
  • PHP

    1. // config/services.php
    2. use App\EventListener\DatabaseActivitySubscriber;
    3. $container->autowire(DatabaseActivitySubscriber::class)
    4. ->addTag('doctrine.event_subscriber')
    5. ;

If you need to associate the subscriber with a specific Doctrine connection, you can do it in the service configuration:

  • YAML

    1. # config/services.yaml
    2. services:
    3. # ...
    4. App\EventListener\DatabaseActivitySubscriber:
    5. tags:
    6. - { name: 'doctrine.event_subscriber', connection: 'default' }
  • XML

    1. <!-- config/services.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
    5. <services>
    6. <!-- ... -->
    7. <service id="App\EventListener\DatabaseActivitySubscriber">
    8. <tag name="doctrine.event_subscriber" connection="default"/>
    9. </service>
    10. </services>
    11. </container>
  • PHP

    1. // config/services.php
    2. use App\EventListener\DatabaseActivitySubscriber;
    3. $container->autowire(DatabaseActivitySubscriber::class)
    4. ->addTag('doctrine.event_subscriber', ['connection' => 'default'])
    5. ;

Tip

Symfony loads (and instantiates) Doctrine subscribers whenever the application executes; whereas Doctrine listeners are only loaded when the related event is actually fired, making them more performant.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.