34

I need to check if a persisted entity has changed and needs to be updated on the database. What I made (and did not work) was the following:

$product = $entityManager->getRepository('Product')->find(3);
$product->setName('A different name');

var_export($entityManager->getUnitOfWork()->isScheduledForUpdate($product));

That code prints always false, I also tried to flush before check the unit of work, but did not work.

Anyone has a suggestion?

eagleoneraptor
  • 1,217
  • 2
  • 12
  • 20
  • What are you trying to accomplish when finding that an entity has changed? – ziad-saab May 29 '12 at 13:32
  • If you look at the source code of `UnitOfWork`, in the comments of `isScheduledForUpdate` they say "Note: Is not very useful currently as dirty entities are only registered at commit time" – ziad-saab May 29 '12 at 13:34
  • Well... I need to send a notification to a system user when the product is changed, but I should ignore the notification when the product data is not changed (I flush anyway after bind the POST data of my form to the entity to use that Doctrine ease). I going to try to execute UnitOfWork::computeChangeSets() before, maybe drop performance but can work. – eagleoneraptor May 29 '12 at 13:43
  • Calling to UnitOfWork::computeChangeSets() before check the update state works, I going to post an answer. – eagleoneraptor May 29 '12 at 13:47

9 Answers9

28

The first thing I'd check it that your setName function is actually doing something ($this-> name = $name...) If it's already working, then you could define an event listener on your services.yml that is triggered when you call the flush.

entity.listener:
  class: YourName\YourBundle\EventListener\EntityListener
  calls:
    - [setContainer,  ["@service_container"]]
  tags:
    - { name: doctrine.event_listener, event: onFlush }

Then you define the EntityListener

namespace YourName\YourBundle\EventListener;

use Doctrine\ORM\Event;
use Symfony\Component\DependencyInjection\ContainerAware;

class EntityListener extends ContainerAware
{   

    /**
     * Gets all the entities to flush
     *
     * @param Event\OnFlushEventArgs $eventArgs Event args
     */
    public function onFlush(Event\OnFlushEventArgs $eventArgs)
    {   
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();

        //Insertions
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
            # your code here for the inserted entities
        }

        //Updates
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            # your code here for the updated entities
        }

        //Deletions
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
            # your code here for the deleted entities
        }
    }
}

If you need to know which entities are being changed, but do something with them after they've been saved to the database, just store the entities changed in a private array, an then define a onFlush event that gets the entities from the array.

BTW, to trigger this kind of events you need to add the @ORM\HasLifecycleCallbacks on the entity.

Thomas Kekeisen
  • 4,355
  • 4
  • 35
  • 54
Sergi
  • 2,872
  • 2
  • 25
  • 24
  • 1
    Keep in mind that entities WILL BE udpated/inserted/deleted and using onFlush event you cannot be sure that flush will be successful. – Jekis Jul 11 '14 at 10:21
  • There is also `$uow->isEntityScheduled()` which covers all operations – Pierre de LESPINAY Mar 06 '15 at 10:17
  • Hi this is exactly what I´m looking for, but with one diff: I need to know what table/data has changed and insert it into another table, something like logging every change into database, can you help me with this? https://stackoverflow.com/questions/44974520/symfony3-save-every-update-into-database – Braian Mellor Jul 28 '17 at 15:12
  • In this comment https://stackoverflow.com/questions/44974520/symfony3-save-every-update-into-database#comment77794270_44985690 you have everything you need to solve the issue ;) – Sergi Aug 01 '17 at 10:31
  • @BraianMellor use instanceof or get_class to get the class of the $entity. Btw, take care not to log the changes in the log table, as if the event listener listens by default to changes in all tables managed by Doctrine (logs table included) – Sergi Aug 01 '17 at 10:38
  • Hi @Sergi, thanks for your answer. I´m using get_class ($entity) and also if ($entity instanceof Changes) return; I´ll read Vadim answer and see if I can go ahead with this. Thanks again – Braian Mellor Aug 01 '17 at 12:33
  • In my script using `$uow->isEntityScheduled()` in a loop was very bad for performance. – Rmy5 Nov 19 '20 at 14:13
  • @Rmy5 I've never measure the performance of the function, but it basically calculates the hash of the entity with spl_object_hash, and check if the has exists in three arrays (entityInsertions, entityUpdates and entityDeletions). – Sergi Nov 30 '20 at 09:28
19

I didn't need/want to create Listeners for my case so I ended up with

$product->setName('A different name');
$uow = $entityManager->getUnitOfWork();
$uow->computeChangeSets();
if ($uow->isEntityScheduled($product)) {
    // My entity has changed
}
Pierre de LESPINAY
  • 44,700
  • 57
  • 210
  • 307
  • Returns true for me when nothing on the entity has changed. – Jessica Mar 17 '15 at 16:47
  • @Jessica, perhaps your entity is changed somewhere else, for example in a life cycle call back. In any case, the object $uow in this answer should tell what has been changed for that entity under the property 'changesets' – Nicolas Nov 07 '16 at 22:25
  • 1
    Heads up, `isEntityScheduled($entity)` returns `true` if a related Entity has changes. E.g. you have `Product#brand`, where `#brand` is a `Brand` Entity. If you modify the `Brand#name` from a child-fieldset of the `ProductFieldset` and have no changes in the base fieldset of `Product`, both the `Product` and `Brand` Entities will have an updated `modifiedAt` timestamp. – rkeet Nov 14 '17 at 12:11
  • @rkeet n it's nice) – Vasilii Suricov Jun 18 '20 at 13:25
  • Work as intented. Make sure to call this before flushing the entity. – kl3sk Jan 27 '21 at 15:36
17

Doctrine2 Docs. 17. Change Tracking Policies

If you use third form (17.3. Notify) as i do, you can test if your entity is changed doing:

$uow = $entityManager->getUnitOfWork();
$uow->computeChangeSets();
$aChangeSet = $uow->getEntityChangeSet($oEntity);

If nothing changed it will return blank array.

Mikl
  • 673
  • 9
  • 19
14

You may also want to look at the PreUpdate event, if you need access to entity fields with their old and new values.

A bit of an example mostly taken from the link provided:

<?php
class NeverAliceOnlyBobListener
{
    public function preUpdate(PreUpdateEventArgs $eventArgs)
    {
        if ($eventArgs->getEntity() instanceof User) {
            if ($eventArgs->hasChangedField('name') && $eventArgs->getNewValue('name') == 'Alice') {
                $oldValue = $eventArgs->getOldValue('name');
                $eventArgs->setNewValue('name', 'Bob');
            }
        }
    }
}
Andrew Atkinson
  • 4,103
  • 5
  • 44
  • 48
3

If you only need to compare old and new state of object then probably this would be simpler:

$originalEntityData = $entityManager->getUnitOfWork()->getOriginalEntityData($entityObject);
Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
2

The issue is quite old but there may be still some group of people that might face this problem from a different point of view. The UnitOfWork works great but it only returns the array of changes. It can be a pain in butt when someone doesn't actually knows which fields may have changed and just wants to get the whole entity as an object to compare $oldEntity and $newEntity. Even though the event's name is preUpdate if someone will try to fetch the data from the database as follows:

$er->find($id);

the returned entity will contain all changes. The workaround is quite simple but it has some hooks:

public function preUpdate(Entity $entity, PreUpdateEventArgs $args)
{
    $entity = clone $entity; //as Doctrine under the hood 
                             //uses reference to manage entities you might want 
                             //to work on the entity copy. Otherwise,        
                             //the below refresh($entity) will affect both 
                             //old and new entity.
    $em = $args->getEntityManager();
    $currentEntity = $em->getRepository('AppBundle:Entity')->find($entity->getId());
    $em->refresh($currentEntity);

}

For those who are using another event, like preFlush, I've quickly checked it and the workaround didn't work well because probably the refresh() method discards any flush changes so what needs to be done is to call the flush once again in listener and create some static $alreadyFlushed toggle to avoid circular reference.

eagleoneraptor
  • 1,217
  • 2
  • 12
  • 20
Mikołaj Król
  • 65
  • 1
  • 2
  • 7
0

Based on my needs, answers here and the docs, I came up with the following solution for a modifiedAt timestamp in an Entity.

/**
 * @Doctrine\ORM\Mapping\PreUpdate()
 *
 * @param \Doctrine\ORM\Event\PreUpdateEventArgs $args
 * @return $this
 */
public function preUpdateModifiedAt(\Doctrine\ORM\Event\PreUpdateEventArgs $args)
{
    $this->setModifiedAt(new \DateTime('now'));

    return $this;
}

This is based on what the docs say about this Event as opposed to the other available ones, such as PostPersist and PreFlush:

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. Note that this event is not triggered when the computed changeset is empty.

Using PreUpdate as opposed to the others lets you leave all the computations and calculation intensive functions to the process already defined by Doctrine. Manually triggering computation of changesets, such as in these answers above are server CPU intensive. The onFlush Event, such as used in the accepted answer is an option (in the way demonstrated), but not if you rely on detecting a change to the Entity, as you can with the function above (preUpdateModifiedAt(PreUpdateEventArgs $args)).

rkeet
  • 3,406
  • 2
  • 23
  • 49
0

I agree with @Andrew Atkinson when he said:

You may also want to look at the PreUpdate event, if you need access to entity fields with their old and new values.

But I disagree with the example he proposed, from my experience, there is a better way to check if something changed or not.

<?php
class Spock
{
    public function preUpdate(PreUpdateEventArgs $eventArgs)
    {
        if (!empty($eventArgs->getEntityChangeSet())) {
            // fill this how you see fit
        }
    }
}

This way the if will only be triggered if there is really some field that changed or not.

As to how to do it if this or that field was changed, then yeah, I recommend his solution.

Rafael
  • 1,495
  • 1
  • 14
  • 25
-1

I am curious about Doctrine and everyone documenting postFlush, as in some case, you have an ongoing transaction. I'd like to point out there's also postTransactionCommit, which could be safer depending on what you're trying to achieve in the postFlush event.

Tristan
  • 149
  • 7