My situation is like this: I have an entity Ticket (like a support ticket) and an entity Action. I am using the Action objects to track any updates on Ticket objects.
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 the
EntityManager#flush()
method
- Changes to associations of the passed entities are not
recognized by the flush operation anymore.
- Any calls to EntityManager#persist()
or
EntityManager#remove()
, even in combination with the UnitOfWork
API are strongly discouraged and don’t work as expected outside the
flush operation.
According the documentation I cannot persist a new object in this event, which is exactly what I need, to persist a new Action object.
onFlush
After some more reading the right event for job appeared:
onFlush
From
documentation:
OnFlush is a very powerful event. It is called inside
EntityManager#flush()
after the changes to all the managed
entities and their associations have been computed. This means, the
onFlush
event has access to the sets of:
...
Entities scheduled for update
...
If you create and persist a new entity in onFlush
, then
calling EntityManager#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.
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$entities = $uow->getScheduledEntityUpdates();
From the entities scheduled to be updated I am interested only on those who are instance of Ticket entity.
if ($entity instanceof Ticket) { ..
Using the
UnitOfWork API we have access to the changes which happend to the Ticket object:
$changes_set = $uow->getEntityChangeSet($entity);
The method
getEntityChangeSet($entity) returns an array, where the keys are the name of the properties who changed.
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/