1
votes

In a Symfony 3 project I have an entity that I want to audit the changes in some of the properties, so I though I could create an Event Listener to store them.

More or less the entity is as follows:

  • ReceivedEmail: agent and caseDetail are the properties I want to audit
  • ReceivedEmailChange: previousAgent, currentAgent and previousCaseDetail and currentCaseDetail

And the EventListener looks as follows

    /**
     * @param OnFlushEventArgs $args
     */
    public function onFlush(OnFlushEventArgs $args)
    {
        /** @var ReceivedEmail $entity */
        $entityManager = $args->getEntityManager();
        $unitOfWork = $entityManager->getUnitOfWork();

        $updates = $unitOfWork->getScheduledEntityUpdates();

        foreach ($updates as $entity) {

            if ($entity instanceof ReceivedEmail) {

                $changes = $unitOfWork->getEntityChangeSet($entity);

                $this->receivedEmailChanges[] = $this->receivedEmailChangeManager
                    ->getReceivedEmailChanges($entity, $changes);
            }
        }
    }

public function postFlush(PostFlushEventArgs $args)
    {
        $em = $args->getEntityManager();

        $i = 0;
        foreach($this->receivedEmailChanges as $receivedEmailChange) {
            $em->persist($receivedEmailChange);
            unset($this->receivedEmailChanges[$i]);
            $i++;
        }

        if ($i > 0) {
            $em->flush();
        }
    }

The problem is calling $entityManager->flush() on the postFlush method ends up in an infinte loop and on this error:

PHP Fatal error: Maximum function nesting level of '256' reached, aborting! in /var/www/sellbytel/vendor/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php on line 187

So my question is: how can I save the changes in the database from the EventListener if it is even possible? If not, is there a workaround to create this log?

1
IMHO you can not use the onFlush event to define an array of changes you need to process and than after processing these changes in postFlush starting the whole process again. Instead of relying on the unitOfWork you can try to replace the onFlush listener by manually saving old and new values using the preUpdate and postUpdate events instead.lordrhodos
Loggable behaviour for Doctrine2 might be of use to you.ccKep

1 Answers

3
votes

If username or password field of User changes, log is written to UserAudit table so same logic as database triggers. You can adjust it to your needs.

Ref: http://www.inanzzz.com/index.php/post/ew0r/logging-field-changes-with-a-trigger-like-event-listener-for-auditing-purposes

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 }

Listener

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