diff --git a/_build/redirection_map b/_build/redirection_map index 3060808842e..183b3d0e9ed 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -457,3 +457,5 @@ /templating/embedding_controllers /templates /templating/inheritance /templates /testing/doctrine /testing/database +/doctrine/lifecycle_callbacks /doctrine/events +/doctrine/event_listeners_subscribers /doctrine/events diff --git a/controller/upload_file.rst b/controller/upload_file.rst index ce63d2bd449..b90c199bab3 100644 --- a/controller/upload_file.rst +++ b/controller/upload_file.rst @@ -353,9 +353,8 @@ Using a Doctrine Listener ------------------------- The previous versions of this article explained how to handle file uploads using -:doc:`Doctrine listeners `. However, this -is no longer recommended, because Doctrine events shouldn't be used for your -domain logic. +:ref:`Doctrine listeners `. However, this is no longer +recommended, because Doctrine events shouldn't be used for your domain logic. Moreover, Doctrine listeners are often dependent on internal Doctrine behavior which may change in future versions. Also, they can introduce performance issues diff --git a/doctrine.rst b/doctrine.rst index 9ffaa964fd9..64a8d15634c 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -812,8 +812,7 @@ Learn more doctrine/associations doctrine/common_extensions - doctrine/lifecycle_callbacks - doctrine/event_listeners_subscribers + doctrine/events doctrine/registration_form doctrine/custom_dql_functions doctrine/dbal diff --git a/doctrine/common_extensions.rst b/doctrine/common_extensions.rst index 3a3f559fc81..c4c27461046 100644 --- a/doctrine/common_extensions.rst +++ b/doctrine/common_extensions.rst @@ -14,8 +14,7 @@ functionality for `Sluggable`_, `Translatable`_, `Timestampable`_, `Loggable`_, The usage for each of these extensions is explained in that repository. However, to install/activate each extension you must register and activate an -:doc:`Event Listener `. -To do this, you have two options: +:ref:`Doctrine event Listener `. To do this, you have two options: #. Use the `StofDoctrineExtensionsBundle`_, which integrates the above library. diff --git a/doctrine/event_listeners_subscribers.rst b/doctrine/event_listeners_subscribers.rst deleted file mode 100644 index de27feecb26..00000000000 --- a/doctrine/event_listeners_subscribers.rst +++ /dev/null @@ -1,267 +0,0 @@ -.. index:: - single: Doctrine; Event listeners and subscribers - -.. _doctrine-event-config: -.. _how-to-register-event-listeners-and-subscribers: - -Doctrine Event Listeners and Subscribers -======================================== - -Doctrine packages have a rich event system that fires events when almost anything -happens inside the system. For you, this means that you can create arbitrary -:doc:`services ` and tell Doctrine to notify those -objects whenever a certain action (e.g. ``prePersist()``) happens within Doctrine. -This could be useful, for example, to create an independent search index -whenever an object in your database is saved. - -Doctrine defines two types of objects that can listen to Doctrine events: -listeners and subscribers. Both are very similar, but listeners are a bit -more straightforward. For more, see `The Event System`_ on Doctrine's documentation. - -Before using them, keep in mind that Doctrine events are intended for -persistence hooks (i.e. *"save also this when saving that"*). They should not be -used for domain logic, such as logging changes, setting ``updatedAt`` and -``createdAt`` properties, etc. - -.. seealso:: - - This article covers listeners and subscribers for Doctrine ORM. If you are - using ODM for MongoDB, read the `DoctrineMongoDBBundle documentation`_. - -Configuring the Listener/Subscriber ------------------------------------ - -To register a service to act as an event listener or subscriber you have -to :doc:`tag ` it with the appropriate name. Depending -on your use-case, you can hook a listener into every DBAL connection and ORM -entity manager or just into one specific DBAL connection and all the entity -managers that use this connection. - -.. configuration-block:: - - .. code-block:: yaml - - services: - # ... - - App\EventListener\SearchIndexer: - tags: - - { name: doctrine.event_listener, event: postPersist } - App\EventListener\SearchIndexer2: - tags: - - { name: doctrine.event_listener, event: postPersist, connection: default } - App\EventListener\SearchIndexerSubscriber: - tags: - - { name: doctrine.event_subscriber, connection: default } - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - .. code-block:: php - - use App\EventListener\SearchIndexer; - use App\EventListener\SearchIndexer2; - use App\EventListener\SearchIndexerSubscriber; - - $container->autowire(SearchIndexer::class) - ->addTag('doctrine.event_listener', ['event' => 'postPersist']) - ; - $container->autowire(SearchIndexer2::class) - ->addTag('doctrine.event_listener', [ - 'event' => 'postPersist', - 'connection' => 'default', - ]) - ; - $container->autowire(SearchIndexerSubscriber::class) - ->addTag('doctrine.event_subscriber', ['connection' => 'default']) - ; - -Creating the Listener Class ---------------------------- - -In the previous example, a ``SearchIndexer`` service was configured as a Doctrine -listener on the event ``postPersist``. The class behind that service must have -a ``postPersist()`` method, which will be called when the event is dispatched:: - - // src/EventListener/SearchIndexer.php - namespace App\EventListener; - - use App\Entity\Product; - // for Doctrine < 2.4: use Doctrine\ORM\Event\LifecycleEventArgs; - use Doctrine\Common\Persistence\Event\LifecycleEventArgs; - - class SearchIndexer - { - public function postPersist(LifecycleEventArgs $args) - { - $entity = $args->getObject(); - - // only act on some "Product" entity - if (!$entity instanceof Product) { - return; - } - - $entityManager = $args->getObjectManager(); - // ... do something with the Product - } - } - -In each event, you have access to a ``LifecycleEventArgs`` object, which -gives you access to both the entity object of the event and the entity manager -itself. - -One important thing to notice is that a listener will be listening for *all* -entities in your application. So, if you're interested in only handling a -specific type of entity (e.g. a ``Product`` entity but not a ``BlogPost`` -entity), you should check for the entity's class type in your method -(as shown above). - -.. tip:: - - In Doctrine 2.4, a feature called Entity Listeners was introduced. - It is a lifecycle listener class used for an entity. You can read - about it in `the DoctrineBundle documentation`_. - -Creating the Subscriber Class ------------------------------ - -A Doctrine event subscriber must implement the ``Doctrine\Common\EventSubscriber`` -interface and have an event method for each event it subscribes to:: - - // src/EventListener/SearchIndexerSubscriber.php - namespace App\EventListener; - - use App\Entity\Product; - use Doctrine\Common\EventSubscriber; - // for Doctrine < 2.4: use Doctrine\ORM\Event\LifecycleEventArgs; - use Doctrine\Common\Persistence\Event\LifecycleEventArgs; - use Doctrine\ORM\Events; - - class SearchIndexerSubscriber implements EventSubscriber - { - public function getSubscribedEvents() - { - return [ - Events::postPersist, - Events::postUpdate, - ]; - } - - public function postUpdate(LifecycleEventArgs $args) - { - $this->index($args); - } - - public function postPersist(LifecycleEventArgs $args) - { - $this->index($args); - } - - public function index(LifecycleEventArgs $args) - { - $entity = $args->getObject(); - - // perhaps you only want to act on some "Product" entity - if ($entity instanceof Product) { - $entityManager = $args->getObjectManager(); - // ... do something with the Product - } - } - } - -.. tip:: - - Doctrine event subscribers cannot return a flexible array of methods to - call for the events like the :ref:`Symfony event subscriber ` - can. Doctrine event subscribers must return a simple array of the event - names they subscribe to. Doctrine will then expect methods on the subscriber - with the same name as each subscribed event, just as when using an event listener. - -For a full reference, see chapter `The Event System`_ in the Doctrine documentation. - -Performance Considerations --------------------------- - -One important difference between listeners and subscribers is that Symfony loads -entity listeners lazily. This means that the listener classes are only fetched -from the service container (and instantiated) if the related event is actually -fired. - -That's why it is preferable to use entity listeners instead of subscribers -whenever possible. - -Priorities for Event Listeners ------------------------------- - -In case you have multiple listeners for the same event you can control the order -in which they are invoked using the ``priority`` attribute on the tag. Priorities -are defined with positive or negative integers (they default to ``0``). Higher -numbers mean that listeners are invoked earlier. - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - App\EventListener\MyHighPriorityListener: - tags: - - { name: doctrine.event_listener, event: postPersist, priority: 10 } - - App\EventListener\MyLowPriorityListener: - tags: - - { name: doctrine.event_listener, event: postPersist, priority: 1 } - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - use App\EventListener\MyHighPriorityListener; - use App\EventListener\MyLowPriorityListener; - - $container - ->autowire(MyHighPriorityListener::class) - ->addTag('doctrine.event_listener', ['event' => 'postPersist', 'priority' => 10]) - ; - - $container - ->autowire(MyLowPriorityListener::class) - ->addTag('doctrine.event_listener', ['event' => 'postPersist', 'priority' => 1]) - ; - -.. _`The Event System`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html -.. _`the DoctrineBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineBundle/entity-listeners.html -.. _`DoctrineMongoDBBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html diff --git a/doctrine/events.rst b/doctrine/events.rst new file mode 100644 index 00000000000..7ce2a798f2a --- /dev/null +++ b/doctrine/events.rst @@ -0,0 +1,445 @@ +.. index:: + single: Doctrine; Lifecycle Callbacks; 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 just 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 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 can't + access to :doc:`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. + +.. seealso:: + + 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 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: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Entity/Product.php + use Doctrine\ORM\Mapping as ORM; + + // When using annotations, don't forget to add @ORM\HasLifecycleCallbacks() + // to the class of the entity where you define the callback + + /** + * @ORM\Entity() + * @ORM\HasLifecycleCallbacks() + */ + class Product + { + // ... + + /** + * @ORM\PrePersist + */ + public function setCreatedAtValue() + { + $this->createdAt = new \DateTime(); + } + } + + .. code-block:: yaml + + # config/doctrine/Product.orm.yml + App\Entity\Product: + type: entity + # ... + lifecycleCallbacks: + prePersist: ['setCreatedAtValue'] + + .. code-block:: xml + + + + + + + + + + + + + +.. 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-listener: + +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:: + + // src/EventListener/SearchIndexer.php + namespace App\EventListener; + + use App\Entity\Product; + use Doctrine\Common\Persistence\Event\LifecycleEventArgs; + + class SearchIndexer + { + // the listener methods receive an argument which gives you access to + // both the entity object of the event and the entity manager itself + public function postPersist(LifecycleEventArgs $args) + { + $entity = $args->getObject(); + + // if this listener only applies to certain entity types, + // add some code to check the entity type as early as possible + if (!$entity instanceof Product) { + return; + } + + $entityManager = $args->getObjectManager(); + // ... do something with the Product entity + } + } + +The next step is to enable the Doctrine listener in the Symfony application by +creating a new service for it and :doc:`tagging it ` +with the ``doctrine.event_listener`` tag: + +.. configuration-block:: + + .. code-block:: yaml + + services: + # ... + + App\EventListener\SearchIndexer: + tags: + - + name: 'doctrine.event_listener' + # this is the only required option for the lifecycle listener tag + event: 'postPersist' + + # listeners can define their priority in case multiple listeners are associated + # to the same event (default priority = 0; higher numbers = listener is run earlier) + priority: 500 + + # you can also restrict listeners to a specific Doctrine connection + connection: 'default' + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + use App\EventListener\SearchIndexer; + + // listeners are applied by default to all Doctrine connections + $container->autowire(SearchIndexer::class) + ->addTag('doctrine.event_listener', [ + // this is the only required option for the lifecycle listener tag + 'event' => 'postPersist', + + // listeners can define their priority in case multiple listeners are associated + // to the same event (default priority = 0; higher numbers = listener is run earlier) + 'priority' => 500, + + # you can also restrict listeners to a specific Doctrine connection + 'connection' => 'default', + ]) + ; + +.. 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:: + + // src/EventListener/UserChangedNotifier.php + namespace App\EventListener; + + use App\Entity\User; + use Doctrine\ORM\Event\PreUpdateEventArgs; + + class UserChangedNotifier + { + // the entity listener methods receive two arguments: + // the entity instance and the lifecycle event + public function postUpdate(User $user, PreUpdateEventArgs $event) + { + // ... do something to notify the changes + } + } + +The next step is to enable the Doctrine listener in the Symfony application by +creating a new service for it and :doc:`tagging it ` +with the ``doctrine.orm.entity_listener`` tag: + +.. configuration-block:: + + .. code-block:: yaml + + services: + # ... + + App\EventListener\UserChangedNotifier: + tags: + - + # these are the basic options that define the entity listener + name: 'doctrine.orm.entity_listener' + event: 'postUpdate' + entity: 'App\Entity\User' + + # set the 'lazy' option to TRUE to only instantiate listeners when they are used + lazy: true + + # you can also associate an entity listener to a specific entity manager + entity_manager: 'custom' + + # by default, Symfony looks for a method called after the event (e.g. postUpdate()) + # if it doesn't exist, it tries to execute the '__invoke()' method, but you can + # configure a custom method name with the 'method' option + method: 'checkUserChanges' + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + use App\Entity\User; + use App\EventListener\UserChangedNotifier; + + $container->autowire(UserChangedNotifier::class) + ->addTag('doctrine.orm.event_listener', [ + // these are the basic options that define the entity listener + 'event' => 'postUpdate', + 'entity' => User::class, + + // set the 'lazy' option to TRUE to only instantiate listeners when they are used + 'lazy' => true, + + // you can also associate an entity listener to a specific entity manager + 'entity_manager' => 'custom', + + // by default, Symfony looks for a method called after the event (e.g. postUpdate()) + // if it doesn't exist, it tries to execute the '__invoke()' method, but you can + // configure a custom method name with the 'method' option + 'method' => 'checkUserChanges', + ]) + ; + +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 listener for the +``postPersist``, ``postRemove`` and ``postUpdate`` Doctrine events:: + + // src/EventListener/DatabaseActivitySubscriber.php + namespace App\EventListener; + + use App\Entity\Product; + use Doctrine\Common\EventSubscriber; + use Doctrine\Common\Persistence\Event\LifecycleEventArgs; + use Doctrine\ORM\Events; + + class DatabaseActivitySubscriber implements EventSubscriber + { + // this method can only return the event names; you cannot define a + // custom method name to execute when each event triggers + public function getSubscribedEvents() + { + return [ + Events::postPersist, + Events::postRemove, + Events::postUpdate, + ]; + } + + // callback methods must be called exactly like the events they listen to; + // they receive an argument of type LifecycleEventArgs, which gives you access + // to both the entity object of the event and the entity manager itself + public function postPersist(LifecycleEventArgs $args) + { + $this->logActivity('persist', $args); + } + + public function postRemove(LifecycleEventArgs $args) + { + $this->logActivity('remove', $args); + } + + public function postUpdate(LifecycleEventArgs $args) + { + $this->logActivity('update', $args); + } + + private function logActivity(string $action, LifecycleEventArgs $args) + { + $entity = $args->getObject(); + + // if this subscriber only applies to certain entity types, + // add some code to check the entity type as early as possible + if (!$entity instanceof Product) { + return; + } + + // ... get the entity information and log it somehow + } + } + +If you're using the :ref:`default services.yaml configuration `, +Symfony will register the Doctrine subscriber automatically thanks to the +:ref:`autoconfigure ` and +:doc:`autowiring ` features. However, if you need +to associate the subscriber with a specific Doctrine connection, you must define +a service for it and :doc:`tag it ` with the +``doctrine.event_subscriber`` tag: + +.. configuration-block:: + + .. code-block:: yaml + + services: + # ... + + # in most applications you don't need to define a service for your + # subscriber (this is only needed when using a custom Doctrine connection) + App\EventListener\DatabaseActivitySubscriber: + tags: + - { name: 'doctrine.event_subscriber', connection: 'default' } + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + use App\EventListener\DatabaseActivitySubscriber; + + // in most applications you don't need to define a service for your + // subscriber (this is only needed when using a custom Doctrine connection) + $container->autowire(DatabaseActivitySubscriber::class) + ->addTag('doctrine.event_subscriber', ['connection' => 'default']) + ; + +.. 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. + +.. _`Doctrine`: https://www.doctrine-project.org/ +.. _`lifecycle events`: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/events.html#lifecycle-events +.. _`official docs about Doctrine events`: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/events.html +.. _`DoctrineMongoDBBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html diff --git a/doctrine/lifecycle_callbacks.rst b/doctrine/lifecycle_callbacks.rst deleted file mode 100644 index 9b62ae16f74..00000000000 --- a/doctrine/lifecycle_callbacks.rst +++ /dev/null @@ -1,100 +0,0 @@ -.. index:: - single: Doctrine; Lifecycle Callbacks - -How to Work with Lifecycle Callbacks -==================================== - -Sometimes, you need to perform an action right before or after an entity -is inserted, updated, or deleted. These types of actions are known as "lifecycle" -callbacks, as they're callback methods that you need to execute during different -stages of the lifecycle of an entity (e.g. the entity is inserted, updated, -deleted, etc). - -If you're using annotations for your metadata, start by enabling the lifecycle -callbacks. This is not necessary if you're using YAML or XML for your mapping. - -.. code-block:: php-annotations - - // src/Entity/Product.php - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\Entity() - * @ORM\HasLifecycleCallbacks() - */ - class Product - { - // ... - } - -Now, you can tell Doctrine to execute a method on any of the available lifecycle -events. For example, suppose you want to set a ``createdAt`` date column to -the current date, only when the entity is first persisted (i.e. inserted): - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Entity/Product.php - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\PrePersist - */ - public function setCreatedAtValue() - { - $this->createdAt = new \DateTime(); - } - - .. code-block:: yaml - - # config/doctrine/Product.orm.yml - App\Entity\Product: - type: entity - # ... - lifecycleCallbacks: - prePersist: [setCreatedAtValue] - - .. code-block:: xml - - - - - - - - - - - - - -.. note:: - - The above example assumes that you've created and mapped a ``createdAt`` - property (not shown here). - -Now, right before the entity is first persisted, Doctrine will automatically -call this method and the ``createdAt`` field will be set to the current date. - -There are several other lifecycle events that you can hook into. For more -information on other lifecycle events and lifecycle callbacks in general, see -Doctrine's `Lifecycle Events documentation`_. - -.. sidebar:: Lifecycle Callbacks and Event Listeners - - Notice that the ``setCreatedAtValue()`` method receives no arguments. This - is always the case for lifecycle callbacks and is intentional: lifecycle - callbacks should be simple methods that are concerned with internally - transforming data in the entity (e.g. setting a created/updated field, - generating a slug value). - - If you need to do some heavier lifting - like performing logging or sending - an email - you should register an external class as an event listener - or subscriber and give it access to whatever resources you need. For - more information, see :doc:`/doctrine/event_listeners_subscribers`. - -.. _`Lifecycle Events documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events diff --git a/reference/dic_tags.rst b/reference/dic_tags.rst index 73a735c736e..df7b61dc2fd 100644 --- a/reference/dic_tags.rst +++ b/reference/dic_tags.rst @@ -205,7 +205,7 @@ doctrine.event_listener **Purpose**: Add a Doctrine event listener For details on creating Doctrine event listeners, read the -:doc:`/doctrine/event_listeners_subscribers` article. +:doc:`Doctrine events ` article. doctrine.event_subscriber ------------------------- @@ -213,7 +213,7 @@ doctrine.event_subscriber **Purpose**: Add a Doctrine event subscriber For details on creating Doctrine event subscribers, read the -:doc:`/doctrine/event_listeners_subscribers` article. +:doc:`Doctrine events ` article. .. _dic-tags-form-type: