12

I want to track changes to a field of a Doctrine Entity. I use Symfony 2.5.0 and Doctrine 2.2.3.

So far i have an EventSubscriber that subscribes to preUpdate. Here I want to create a new Entity which stores the new and old value and holds a reference to the Entity that is updated.

The problem is, that I can't find a way to persist this new Entity. If I persist() in preUpdate and flush() in postUpdate, it works if I change only one Entity. If multiple Entities are changed, I get an error that the changeset is empty.

I tried fiddling with different events with different results. Blank pages, tracking entites do not get persisted etc.

I think that this should be a common use case - but I can't find examples.

Joshua
  • 2,932
  • 2
  • 24
  • 40

2 Answers2

24

Don't use preUpdate or postUpdate, you will have problems. Take a look at onFlush instead.

You have access to the complete changeset at this point, so you can find out what fields have changed, what's been added etc. You can also safely persist new entities. Note as the docs say, you will have to recompute change sets when you persist or change entities.

Simple example I knocked together, not tested but something similar to this will get you what you want.

public function onFlush(OnFlushEventArgs $args) {

    $entityManager = $args->getEntityManager();
    $unitOfWork = $entityManager->getUnitOfWork();
    $updatedEntities = $unitOfWork->getScheduledEntityUpdates();

    foreach ($updatedEntities as $updatedEntity) {

        if ($updatedEntity instanceof YourEntity) {

            $changeset = $unitOfWork->getEntityChangeSet($updatedEntity);

            if (array_key_exists('someFieldInYourEntity', $changeset)) {

                $changes = $changeset['someFieldInYourEntity'];

                $previousValueForField = array_key_exists(0, $changes) ? $changes[0] : null;
                $newValueForField = array_key_exists(1, $changes) ? $changes[1] : null;

                if ($previousValueForField != $newValueForField) {

                    $yourChangeTrackingEntity = new YourChangeTrackingEntity();
                    $yourChangeTrackingEntity->setSomeFieldChanged($previousValueForField);
                    $yourChangeTrackingEntity->setSomeFieldChangedTo($newValueForField);

                    $entityManager->persist($yourChangeTrackingEntity);
                    $metaData = $entityManager->getClassMetadata('YourNameSpace\YourBundle\Entity\YourChangeTrackingEntity');
                    $unitOfWork->computeChangeSet($metaData, $yourChangeTrackingEntity);
                }
            }
        }
    }
}
Richard
  • 4,079
  • 1
  • 14
  • 17
  • tank you, I built it similar to your example. it works like a charm now. – Joshua Jul 23 '15 at 06:09
  • if (!is_array($changeset)) { return null; } Should that be continue? – jgmjgm Feb 05 '20 at 18:42
  • @jgmjgm no, because in the example above a) you have found the specific entity class you want to check for changes and b) there are no changes, so you're done. Continue won't break anything, but will add needless extra iterations. If you were checking multiple different entity classes, continue would make sense. – Richard Feb 07 '20 at 00:55
  • A better question: Why would changeset be not an array and why wouldn't there be another entity of the same type later on after encountering that condition? – jgmjgm Feb 07 '20 at 12:24
  • According to the docs and with a glance at the code, change set is supposed to always be an array so the if condition might not be necessary. If it is, then it might be version dependent, hole in the documentation or even a minor bug. – jgmjgm Feb 07 '20 at 15:35
2

You might be interested in EntityAudit bundle.

It allows to configure which entities should be tracked. Then it introduces a concept of revisions of the database. Each revision has timestamp, username and list of entities affected.

Then, you can find all revisions that affect particular entity:

$revisions = $auditReader->findRevisions('AppBundle\Entity\Article', 1);

or instantiate it in a particular revision:

$oldArticle = $auditReader->find(
  'AppBundle\Entity\Article',
  $id = 1,
  $rev = 2
);

so you can easily compare the current and the old state of the entity.

The bundle also ships with example views demonstrating how to display revision list, compare objects in different versions and more.

selecting comparison

comparison view

fracz
  • 20,536
  • 18
  • 103
  • 149