Showing posts with label UnitOfWork. Show all posts
Showing posts with label UnitOfWork. Show all posts

Wednesday, April 20, 2016

Doctrine Events: on Flush - get entity changes and persist new object

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/