Quickly create entities logs with onFlush method on Symfony

The more I develop, the more I'm logging everything I code.

These can be logs for the backend, but also histories for the users. A simple way to create a history is to log all changes made to the database.

Doctrine gives us access to the method onFlush to get all data that will be updated, added, or deleted before flushing.

This is a guide to quickly create logs inside Doctrine methods.

Connect onFlush method

Nothing here is out of usual things, we connect on the method onFlush of Doctrine.

First, we will connect to the Doctrine event.

#config\services.yaml
App\EventListener\DatabaseOnFlushListener:
    tags:
        - # these are the options required to define the entity listener
            name: 'doctrine.event_listener'
            event: 'onFlush'

Then, we create our class with our onFlush method.

# src\EventListener\DatabaseOnFlushListener.php
class DatabaseOnFlushListener
{
    /**
     * @param OnFlushEventArgs $eventArgs
     */
    public function onFlush(OnFlushEventArgs $eventArgs): void
    {
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();
        // We will add methods there
    }
}

UnitOfWork contains database context, meaning all modifications that will be applied to our database.

Observe entities changes

# src\EventListener\DatabaseOnFlushListener.php
class DatabaseOnFlushListener
{
    /**
     * @param OnFlushEventArgs $eventArgs
     */
    public function onFlush(OnFlushEventArgs $eventArgs): void
    {
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();
        
        foreach ($uow->getScheduledEntityUpdates() as $entity) {

        }
		//... Check the documentation to get others methods 
    }
}

Check the documentation to read more about available methods on UnitOfWork :

Events - Doctrine Object Relational Mapper (ORM)
Doctrine Object Relational Mapper Documentation: Events

We now have access to :

  • instanceof to filter on an entity type
  • getEntityChangeSet to compare old and new values
  • computeChangeSet to insert a new entity
  • recomputeSingleEntityChangeSet to update an existing entity (be really careful here)

Let's take an example: we want to create a log every time we update a user.

# src\EventListener\DatabaseOnFlushListener.php
class DatabaseOnFlushListener
{
    /**
     * @param OnFlushEventArgs $eventArgs
     */
    public function onFlush(OnFlushEventArgs $eventArgs): void
    {
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();
        
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            if ($entity instanceof User) {
                $uow->computeChangeSets();
                // We check if our entity contains some changes
                $changeSet = $uow->getEntityChangeSet($entity);
                if ($changeSet) {
                    $log = new Log();
                    $em->persist($log);
                    $uow->computeChangeSet($em->getClassMetadata(get_class($log)), $log);
                }
            }
        }
    }
}

We get changeSet to check if the entity contains changes, then we create a new object Log that we insert.

But changeSet contains more information. It allows checking on each field of the user entity.

We suppose that the class User has one field enabled, and we only want to log when the user is moving from enabled to disabled

changeSet contains an index enabled is an array with 2 values :

  • [0] contains the old value
  • [1] contains the new value
if ($changeSet && isset($changeSet['enabled']) && $changeSet['enabled'][1] === false) {
    $log = new Log();
    $em->persist($log);
    $uow->computeChangeSet($em->getClassMetadata(get_class($log)), $log);
}

Summary

onFlush is quite an easy way to create logs on your entities since each time your entity is updated, it will call your function inside.

But, and this is a huge but!

Even if it's possible, I do not recommend modifying your entities inside the onFlush method.

Instead, update all your data in your service.

You should not add any logic inside onFlush, since as the name suggests, it is only used to flush your database.