0

I need to create changelog in the API for user actions on entities.

For example:

User updates entity Licensor I need to catch the changes and save them in the database in different table.

The first part I was able to do with Doctrine Event Listener

class ChangelogEventListener
{
   public function preUpdate($obj, PreUpdateEventArgs $eventArgs)
   {
       if ($obj instanceof LoggableInterface) {
            dump($eventArgs->getEntityChangeSet());
       }
   }
}

And with marking entity event listeners

/**
 * @ORM\EntityListeners(value={"AppBundle\EventSubscriber\Changelogger\ChangelogEventListener"})
 */
class Licensor implements LoggableInterface

But I'm not sure if it's even possible and if it makes sense to access the ORM entity manager in a preUpdate event.

If it isn't then what's the proper way to do it?

I've tried with Symfony's EventListener instead of Doctrine's but then I don't have access to getEntityChangeSet().

Jason Roman
  • 8,146
  • 10
  • 35
  • 40
Robert
  • 19,800
  • 5
  • 55
  • 85

2 Answers2

3

Check out Doctrine events, and specifically the preUpdate event. This event is the most restrictive, but you do have access to all of the fields that have changed, and their old/new values. You can change the values here on the entity being updated, unless it's an associated entity.

Check out this answer, which suggests using an event subscriber, and then persisting to a logging entity.

There is also this blog post that uses the preUpdate event to save a bunch of changesets to the internal listener class, then postFlush it persists any entities that are being changed, and calls flush again. However, I would not recommend this, as the Doctrine documentation explicitly states:

postFlush is called at the end of EntityManager#flush(). EntityManager#flush() can NOT be called safely inside its listeners.

If you went the route of that blog post you'd be better off using the onFlush() event and then doing your computeChangeSets() call after your persist(), like the first answer I posted.

You can find a similar example here:

Jason Roman
  • 8,146
  • 10
  • 35
  • 40
  • Can I call $uow->getEntityChangeSet(); inside onFlush method is it safe? – Robert Jul 11 '17 at 07:35
  • You should be able to yes. – Jason Roman Jul 11 '17 at 12:37
  • 1
    I was able to do it on onFlush and postFlush events because when the objects were created because I don't use uuid I didn't have ids in onFlush method. I'll accept your answer as directed me to the proper solution. Thanks. – Robert Jul 12 '17 at 10:02
2

You are better off using an event listener for such thing. What you want is more like a database trigger to log changes. See example below (tested and works fine) which logs User entity changes in UserAudit entity. For demonstration purposes, it only watches username and password field but you can modify it as you wish.

Note: If you want an entity listener then look at this example.

services.yml

services:
    application_backend.event_listener.user_entity_audit:
        class: Application\BackendBundle\EventListener\UserEntityAuditListener
        arguments: [ @security.context ]
        tags:
            - { name: doctrine.event_listener, event: preUpdate }
            - { name: doctrine.event_listener, event: postFlush }

UserEntityAuditListener

namespace Application\BackendBundle\EventListener;

use Application\BackendBundle\Entity\User;
use Application\BackendBundle\Entity\UserAudit;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Symfony\Component\Security\Core\SecurityContextInterface;

class UserEntityAuditListener
{
    private $securityContext;
    private $fields = ['username', 'password'];
    private $audit = [];

    public function __construct(SecurityContextInterface $securityContextInterface)
    {
        $this->securityContext = $securityContextInterface;
    }

    public function preUpdate(PreUpdateEventArgs $args) // OR LifecycleEventArgs
    {
        $entity = $args->getEntity();

        if ($entity instanceof User) {
            foreach ($this->fields as $field) {
                if ($args->getOldValue($field) != $args->getNewValue($field)) {
                    $audit = new UserAudit();
                    $audit->setField($field);
                    $audit->setOld($args->getOldValue($field));
                    $audit->setNew($args->getNewValue($field));
                    $audit->setUser($this->securityContext->getToken()->getUsername());

                    $this->audit[] = $audit;
                }
            }
        }
    }

    public function postFlush(PostFlushEventArgs $args)
    {
        if (! empty($this->audit)) {
            $em = $args->getEntityManager();

            foreach ($this->audit as $audit) {
                $em->persist($audit);
            }

            $this->audit = [];
            $em->flush();
        }
    }
}
BentCoder
  • 12,257
  • 22
  • 93
  • 165
  • I'm not sure about this answer because it's mentioned on Doctrine documentation PostFlush: "EntityManager#flush() can NOT be called safely inside its listeners." – Robert Jul 11 '17 at 11:09
  • That additional flush is strictly for flushing your audit entity, not the original one. Original one is already flushed before the additional flush. That particular sentence in documentation is superficial and cut short. You can always try and see how it works before coming to a conclusion. Example worked and still works perfectly fine for many years for me. – BentCoder Jul 11 '17 at 12:23
  • why don´t you use getEntityChangeSet? – Braian Mellor Aug 01 '17 at 15:12