Robert
Robert

Reputation: 20286

Saving the getEntityChangeSet() result in Doctrine EventListener

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().

Upvotes: 0

Views: 5386

Answers (2)

BentCoder
BentCoder

Reputation: 12750

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();
        }
    }
}

Upvotes: 3

Jason Roman
Jason Roman

Reputation: 8276

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:

Upvotes: 3

Related Questions