18

I want add new Feed item on entity persist and update. I write this event listener (postUpdate is same):

public function postPersist(LifecycleEventArgs $args)
{
    $entity = $args->getEntity();
    $em = $args->getEntityManager();

    if ($entity instanceof FeedItemInterface) {
        $feed = new FeedEntity();
        $feed->setTitle($entity->getFeedTitle());
        $feed->setEntity($entity->getFeedEntityId());
        $feed->setType($entity->getFeedType());
        if($entity->isFeedTranslatable()) {
            $feed->getEnTranslation()->setTitle($entity->getFeedTitle('en'));
        }
        $em->persist($feed);
        $em->flush();
    }
}

But I got

Integrity constraint violation: 1062 Duplicate entry '30-2' for key 'PRIMARY'

and in log a have two insertations:

INSERT INTO interview_scientificdirection (interview_id, scientificdirection_id) VALUES (?, ?) ([30,2]) INSERT INTO interview_scientificdirection (interview_id, scientificdirection_id) VALUES (?, ?) ([30,2])

scientificdirection is Many to Many relationship table for entity what we want to persist. In frontend application everything work fine, but in Sonata Admin I got this problem :(

Darryl Hein
  • 142,451
  • 95
  • 218
  • 261
nucleartux
  • 1,381
  • 1
  • 14
  • 36

4 Answers4

34

If you need to persist additional objects, the postPersist or postUpdate handler in Doctrine is, sadly, not the right place to go. I struggled with the same problem today, as I needed to generate some message entries in that handler.

The problem at this point is that the postPersist handler is called during the flush event, and not after. So you can't persist additional objects here, as they are not getting flushed afterwards. Additionally, you can't call flush during an postPersist handler, as this might lead to ducplicate entries (as you have experienced).

One way to go is using the onFlush handler from doctrine, documented here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/events.html#onflush

This is just problematic if you need the inserted ids of the database object, as the entity hasn't yet been written to the database in that handler. If you don't need those ids, you are fine with the onFlush event in doctrine.

For me, the solution was a little different. I'm currently working on a symfony2 project, and needed the ids of the inserted database objects (for callbacks and updates later on).

I created a new service in symfony2, which basically just acts like a queue for my messages. During the postPersist update, I just fill the entries in the queue. I have another handler registered on kernel.response, which then takes those entries and persists them to the database. (Something along the line of this: http://symfony.com/doc/current/cookbook/service_container/event_listener.html)

I hope I don't digress too much from the topic here, but as it is something I really struggled with, I hope that some people might find this useful.

The service entries for this are:

 amq_messages_chain:
   class: Acme\StoreBundle\Listener\AmqMessagesChain

 amqflush:
   class: Acme\StoreBundle\Listener\AmqFlush
   arguments: [ @doctrine.orm.entity_manager, @amq_messages_chain, @logger ]
   tags:
     - { name: kernel.event_listener, event: kernel.response, method: onResponse, priority: 5 }

 doctrine.listener:
  class: Acme\StoreBundle\Listener\AmqListener
  arguments: [ @logger, @amq_messages_chain ]
  tags:
    - { name: doctrine.event_listener, event: postPersist }
    - { name: doctrine.event_listener, event: postUpdate }
    - { name: doctrine.event_listener, event: prePersist }

You can't use the doctrine.listener for this, as this leads to a circular dependency (as you need the entity manager for the service, but the entity manager needs the service....)

That worked like a charm. If you need more info on that, don't hesitate to ask, I'm glad to add some examples to this.

Stephan Vierkant
  • 9,674
  • 8
  • 61
  • 97
jhoffrichter
  • 516
  • 4
  • 8
  • You want say to create a record in the event and know it ID message queue needed? – nucleartux Jun 15 '12 at 20:04
  • No, I needed the IDs of the entity which is just about to be persisted in the onFlush handler. And as the unit of work is just being prepared, you don't have the ids of the entities which will be flushed after the onFlush handler is finished. Or did I misunderstand your question? – jhoffrichter Jun 15 '12 at 22:55
  • Yes, you misunderstand me :) Can I create new entity in event if I need Id of entity what occurs this event without message queueing software? – nucleartux Jun 16 '12 at 17:29
  • @jhoffrichter why are you attaching your listener to a request event? What if this code is executed in a console? Have you tried with Doctrine's postFlush? – Francesc Rosas Jun 27 '12 at 17:01
  • This is an interesting solution and I may use it as well. In my case, I'm using Doctrine lifecycle events to detect changes to properties on several entities and then logging them by creating a new log entity and trying to persist it to the database. "Queuing" the logs I need to make and then persisting them on a Kernel event should work well. – Brian Feb 17 '16 at 16:45
  • Very clever solution ! – Djagu Nov 22 '17 at 08:57
31

The answer from Francesc is wrong, as the changesets in the postFlush event are already empty. The second answer of jhoffrichter could work, but is overkill. The right way to go is to persist the entity in the postPersist event and to call flush again in the postFlush event. But you have to do this only if you changed something in the postPersist event, otherwise you create an endless loop.

public function postPersist(LifecycleEventArgs $args) {

    $entity = $args->getEntity();
    $em = $args->getEntityManager();

    if($entity instanceof FeedItemInterface) {
        $feed = new FeedEntity();
        $feed->setTitle($entity->getFeedTitle());
        $feed->setEntity($entity->getFeedEntityId());
        $feed->setType($entity->getFeedType());
        if($entity->isFeedTranslatable()) {
            $feed->getEnTranslation()->setTitle($entity->getFeedTitle('en'));
        }
        $em->persist($feed);
        $this->needsFlush = true;
    }
}

public function postFlush(PostFlushEventArgs $eventArgs)
{
    if ($this->needsFlush) {
        $this->needsFlush = false;
        $eventArgs->getEntityManager()->flush();
    }
}
chris
  • 389
  • 3
  • 3
  • This ain't good, because this way - you will make two distinct transactions. Hence only one of them may fail leaving just one entity. That is - unless you explicitly do $em->beginTransaction(); – M. Ivanov Sep 01 '15 at 21:57
  • 11
    Hey @chris, I'm not trying to be a hater, but the Doctrine [documentation](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#postflush) says "EntityManager#flush() can NOT be safely called inside [postFlush's] listeners." What are your thoughts on this? – Ian Phillips Oct 22 '15 at 16:38
  • 3
    @chris, as Ian said above, flush() inside postFlush() isn't safe. Does this really works? It seems to me that it doesn't. – Nuno Pereira Feb 09 '16 at 17:16
3

The solution from jhoffrichter is working very well. If you use Console Commands you should add a tag for the event command.terminate. Otherwise it is not working inside Console Commands. See https://stackoverflow.com/a/19737608/1526162

config.yml

amq_messages_chain:
   class: Acme\StoreBundle\Listener\AmqMessagesChain

amqflush:
   class: Acme\StoreBundle\Listener\AmqFlush
   arguments: [ @doctrine.orm.entity_manager, @amq_messages_chain, @logger ]
   tags:
     - { name: kernel.event_listener, event: kernel.response, method: onResponse, priority: 5 }
     - { name: kernel.event_listener, event: command.terminate, method: onResponse }

doctrine.listener:
  class: Acme\StoreBundle\Listener\AmqListener
  arguments: [ @logger, @amq_messages_chain ]
  tags:
    - { name: doctrine.event_listener, event: postPersist }
    - { name: doctrine.event_listener, event: postUpdate }
    - { name: doctrine.event_listener, event: prePersist }
Community
  • 1
  • 1
Stefan Bergfeld
  • 113
  • 2
  • 9
2

Well, heres how i have done in SF 2.0 and 2.2:

Listener class:

<?php
namespace YourNamespace\EventListener;

use Doctrine\ORM\Mapping\PostPersist;


/*
 * ORMListener class
 *
 * @author:        Marco Aurélio Simão
 * @description:   Listener para realizar operações em qualquer objeto manipulado pelo Doctrine 2.2
 */

use Doctrine\ORM\UnitOfWork;

use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\Common\EventArgs;
use Doctrine\ORM\Mapping\PrePersist;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Mapping\PostUpdate;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Enova\EntitiesBundle\Entity\Entidades;

use Doctrine\ORM\Event\LifecycleEventArgs;

use Enova\EntitiesBundle\Entity\Tagged;
use Enova\EntitiesBundle\Entity\Tags;

class ORMListener
{
    protected $extra_update;

    public function __construct($container)
    {
        $this->container    = $container;
        $this->extra_update = false;
    }

    public function onFlush(OnFlushEventArgs $args)
    {
        $securityContext = $this->container->get('security.context');
        $em              = $args->getEntityManager();

        $uow             = $em->getUnitOfWork();
        $cmf             = $em->getMetadataFactory();

        foreach ($uow->getScheduledEntityInsertions() AS $entity)
        {
            $meta = $cmf->getMetadataFor(get_class($entity));

            $this->updateTagged($em, $entity);
        }

        foreach ($uow->getScheduledEntityUpdates() as $entity)
        {
            $meta = $cmf->getMetadataFor(get_class($entity));

            $this->updateTagged($em, $entity);
        }
    }

    public function updateTagged($em, $entity)
    {
      $entityTags = $entity->getTags();

      $a = array_shift($entityTags);
      //in my case, i have already sent the object from the form, but you could just replace this part for new Object() etc

      $uow      = $em->getUnitOfWork();
      $cmf      = $em->getMetadataFactory();
      $meta     = $cmf->getMetadataFor(get_class($a));

      $em->persist($a);

      $uow->computeChangeSet($meta, $a);
    }

}

Config.yml:

services:
    updated_by.listener:
        class: YourNamespace\EventListener\ORMListener
        arguments: [@service_container]
        tags:
            - { name: doctrine.event_listener, event: onFlush, method: onFlush }

Hope it helps ;)

Marco
  • 2,757
  • 1
  • 19
  • 24