BentCoder
BentCoder

Reputation: 12740

Creating preUpdate or preFlush event with Event Listener

I have a onFlush() event which works fine but what I need to do is to turn that into preFlush() or preUpdate() both acceptable. I did preFlush() but for some reason it doesn't do anything. Not even an error. What am I missing?

TEST: I placed exit in preFlush() to see if it is being called at all or not. Outcome is: 1 so foreach() is never run! It is an empty array. I also tested preUpdate() and all the lines in that get runed but no data inserted.

public function preFlush(PreFlushEventArgs $args)
{
    $em = $args->getEntityManager();
    $uow = $em->getUnitOfWork();
    echo '1';
    foreach ($uow->getScheduledEntityUpdates() as $entity) {
        echo '2';
        if ($entity instanceof User) {
            echo '3';
        }
    }
    exit;
}

I created them after reading the documentation.

service.yml

services:
    entity.event_listener.user:
        class:  Site\FrontBundle\EventListener\Entity\UserListener
        tags:
            - { name: doctrine.event_listener, event: preUpdate }
            - { name: doctrine.event_listener, event: onFlush }
            - { name: doctrine.event_listener, event: preFlush }

Working onFlush() example:

class UserListener
{
    public function onFlush(OnFlushEventArgs $args)
    {
        $em = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            if ($entity instanceof User) {
                $userLog = new UserLog();
                $userLog->setDescription($entity->getId() . ' being updated.');

                $em->persist($userLog);

                // Instead of $em->flush() cos we're already in flush process
                $userLogMetadata = $em->getClassMetadata(get_class($userLog));
                $uow->computeChangeSet($userLogMetadata, $userLog);
            }
        }
    }
}

Not working preFlush() example:

class UserListener
{
    public function preFlush(PreFlushEventArgs $args)
    {
        $em = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            if ($entity instanceof User) {
                $userLog = new UserLog();
                $userLog->setDescription($entity->getId() . ' being updated.');

                $em->persist($userLog);

                // Instead of $em->flush() cos we're already in flush process
                $userLogMetadata = $em->getClassMetadata(get_class($userLog));
                $uow->computeChangeSet($userLogMetadata, $userLog);
            }
        }
    }
}

Not working preUpdate() example

class UserListener
{
    public function preUpdate(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        $em = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        if ($entity instanceof User) {
            $userLog = new UserLog();
            $userLog->setDescription($entity->getId() . ') been updated.');

            $em = $args->getEntityManager();
            $em->persist($userLog);
            $userLogMetadata = $em->getClassMetadata(get_class($userLog));
            $uow->computeChangeSet($userLogMetadata, $userLog);
        }
    }
}

Upvotes: 6

Views: 25293

Answers (3)

De Nguyen
De Nguyen

Reputation: 417

Dear BentCoder:
I'm using Symfony 2.7 version. When using $em->flush in event listener like your post, this error was occur: click-here-to-see-bug-description

And, this is my solution:

Service.yml

services:
    app.listener:
        class: AppBundle\EventListener\DoctrineListener
        arguments: ["@service_container"]
        tags:
            - { name: doctrine.event_listener, event: preUpdate, method: preUpdate }
            - { name: doctrine.event_listener, event: postUpdate, method: postUpdate }

Event Listener

namespace AppBundle\EventListener;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;

use AppBundle\Entity;

/**
 * Log activity on website
 * Class DoctrineListener
 * @package AppBundle\EventListener
 */
class DoctrineListener
{
    /**
     * @var ContainerInterface
     */
    private $_container;

    /**
     * @var Array
     */
    private $_activities;

    /**
     * DoctrineListener constructor.
     * @param ContainerInterface $container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->_container = $container;
    }

    /**
     * @param LifecycleEventArgs $args
     */
    public function preUpdate(LifecycleEventArgs $args)
    {
        $entityManager = $args->getEntityManager();
        $entity = $args->getEntity();
        $activityEntity = new Entity\Activity();
        $activityEntity->setAction(Entity\Activity::ACTION_EDIT);
        $activityEntity->setActionAt(new \DateTime());
        $activityEntity->setIpAddress($this->_container->get('request')->getClientIp());
        switch (true) {
            case $entity instanceof Entity\Goods:
                $repository = $entityManager->getRepository('AppBundle:Goods');
                $activityEntity->setType(Entity\Activity::TYPE_GOODS);
                $message = 'User: <strong>%s</strong> sửa mẫu hàng hóa <strong>%s</strong>';
                break;
            default:
                return;
        }
        if (isset($repository) && $args->getEntityChangeSet()) {
            $user = $this->_container->get('security.context')->getToken()->getUser();
            $recordBefore = clone $entity;
            foreach ($args->getEntityChangeSet() as $key => $value) {
                $method = 'set'.ucfirst($key);
                $recordBefore->$method($value[0]);
            }
            $activityEntity->setFosUser($user);
            $activityEntity->setRecordBefore(serialize($recordBefore));
            $activityEntity->setRecordAfter(serialize($entity));
            $activityEntity->setMessage(
                sprintf(
                    $message,
                    $user->getUserProfile()->getFullName(),
                    (string) $recordBefore
                )
            );
            $this->_activities[] = $activityEntity;
        }
        return;
    }

    /**
     * @param LifecycleEventArgs $args
     */
    public function postUpdate(LifecycleEventArgs $args)
    {
        if (sizeof($this->_activities)) {
            $entityManager = $args->getEntityManager();
            foreach ($this->_activities as $activity) {
                $entityManager->persist($activity);
            }
            $entityManager->flush();
        }
    }
}

Hope this will help someone!

Upvotes: 1

BentCoder
BentCoder

Reputation: 12740

SOLUTION:

The trick is, persisting after preUpdate() within postFlush() event.

Note: Although this might not be the best solution, it answers the question however it could be done with an Event Subscriber or simple onFlush() -> $uow->getScheduledEntityUpdates() in an Event Listener.

Service.yml

services:

    entity.event_listener.user_update:
        class:  Site\FrontBundle\EventListener\Entity\UserUpdateListener
        tags:
            - { name: doctrine.event_listener, event: preUpdate }
            - { name: doctrine.event_listener, event: postFlush }

Event Listener

<?php

namespace Site\FrontBundle\EventListener\Entity;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Site\FrontBundle\Entity\User;
use Site\FrontBundle\Entity\UserLog;

class UserUpdateListener
{
    private $log = array();

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

        // False check is compulsory otherwise duplication occurs
        if (($entity instanceof User) === false) {
            $userLog = new UserLog();
            $userLog->setDescription($entity->getId() . ' being updated.');

            $this->log[] = $userLog;
        }
    }

    public function postFlush(PostFlushEventArgs $args)
    {
        if (! empty($this->log)) {
            $em = $args->getEntityManager();
            foreach ($this->log as $log) {
                $em->persist($log);
            }
            $em->flush();
        }
    }
} 

Upvotes: 13

Bartłomiej Wach
Bartłomiej Wach

Reputation: 1986

Reading the docs,

http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#onflush

it doesn't mention that preFlush has the infor about changes(I mean the entityManager)

if you look at Doctrine\ORM\UnitOfWork you see that change sets are computed after preFlush event so you should use onFlush if you want to interact with changed entities

// Raise preFlush
    if ($this->evm->hasListeners(Events::preFlush)) {
        $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
    }

// Compute changes done since last commit.
if ($entity === null) {
    $this->computeChangeSets();
} elseif (is_object($entity)) {
    $this->computeSingleEntityChangeSet($entity);
} elseif (is_array($entity)) {
    foreach ($entity as $object) {
        $this->computeSingleEntityChangeSet($object);
    }
}

Upvotes: 2

Related Questions