Each time a Ticket is updated I want to create and persist a new Action object. The newly created Action object should contain the Ticket (there is a ManyToOne relation between Action and Ticket), and a note saying "The property X changed. The new value is: Y".
preUpdate
Looking over the available Doctrine events, my first thought was that preUpdate should be right for the job:
preUpdate - The preUpdate event occurs before the database update operations to entity data. It is not called for a DQL UPDATE statement nor when the computed changeset is empty.Reading more in detail the documentation I found that preUpdate event has many limitations:
- PreUpdate is the most restrictive to use event, since it is called right before an update statement is called for an entity inside theAccording the documentation I cannot persist a new object in this event, which is exactly what I need, to persist a new Action object.EntityManager#flush()
method
- Changes to associations of the passed entities are not recognized by the flush operation anymore.
- Any calls toEntityManager#persist()
orEntityManager#remove()
, even in combination with the UnitOfWork API are strongly discouraged and don’t work as expected outside the flush operation.
onFlush
After some more reading the right event for job appeared: onFlush
From documentation:
OnFlush is a very powerful event. It is called insideEntityManager#flush()
after the changes to all the managed entities and their associations have been computed. This means, theonFlush
event has access to the sets of:
...
Entities scheduled for update
...
If you create and persist a new entity inonFlush
, then callingEntityManager#persist()
is not enough. You have to execute an additional call to$unitOfWork->computeChangeSet($classMetadata, $entity)
.
So in this event I have access the UnitOfWork, which means I get access to all entities scheduled to be updated and also I can persist a new object doing that additional call. Great!
Implementation
In my Symfony project I've created a new listener class called TicketListener with a method onFlush:
<?php
use Doctrine\ORM\Event\OnFlushEventArgs;
class TicketListener { public function onFlush(OnFlushEventArgs $args) { }
And registered it as a service with Doctrine tag:
doctrine.ticket_listener: class: MyBundle\EventListeners\TicketListener arguments: [] tags: - { name: doctrine.event_listener, event: onFlush }
Using the OnFlushEventArgs we can access the Entity Manager and the UnitOfWork.
From the entities scheduled to be updated I am interested only on those who are instance of Ticket entity.$em = $args->getEntityManager(); $uow = $em->getUnitOfWork();$entities = $uow->getScheduledEntityUpdates();
Using the UnitOfWork API we have access to the changes which happend to the Ticket object:if ($entity instanceof Ticket) { ..
The method getEntityChangeSet($entity) returns an array, where the keys are the name of the properties who changed.$changes_set = $uow->getEntityChangeSet($entity);
When accessing an array key, you get another array with 2 positions [0] and [1], [0] contains the old value, [1] contains the new value.
In order to persist the new object, additionally to $em->persist() the following code need to be excuted
$classMetadata = $em->getClassMetadata('MyBundle\Entity\Action'); $uow->computeChangeSet($classMetadata, $action);
Below is the complete example:
public function onFlush(OnFlushEventArgs $args) { $em = $args->getEntityManager(); $uow = $em->getUnitOfWork(); // get only the entities scheduled to be updated $entities = $uow->getScheduledEntityUpdates(); foreach ($entities as $entity) { //continue only if the object to be updated is a Ticket if ($entity instanceof Ticket) { //get all the changed properties of the Ticket object $changes_set = $uow->getEntityChangeSet($entity); $changes = array_keys($changes_set); foreach ($changes as $changed_property) { $action = new Action(); $action->setTicket($entity); $text = ucfirst($changed_property) . ' changed! New value: ' . $changes_set[$changed_property][1]; $action->setDescription($text); $em->persist($action); $classMetadata = $em->getClassMetadata('MyBundle\Entity\Action'); $uow->computeChangeSet($classMetadata, $action); } } }
This article was very helpful for me: http://vvv.tobiassjosten.net/symfony/update-associated-entities-in-doctrine/