Infinite loop in Doctrine event listener when tryi

2019-07-14 05:48发布

问题:

I want that every time a new Distance entity (from Place_A to Place_B) is saved, the reverse distance (from Place_B to Place_A) gets inserted too into the DB.

My problem is the following listener loops infinitely (hence the counter):

class Listener
{
    public $count;

    public function prePersist(LifecycleEventArgs $eventArgs)
    {
        if ($this->count > 5) {
            die();
        }

        $entity = $eventArgs->getEntity();

        if ($entity instanceof Distance) {
            // $this->created = microtime(true) in Distance's constructor
            echo 'Entity created at ' . $entity->created;

            if ($entity->isReverse) {
                echo " is reverse\n";
            } else {
                echo " is not reverse\n";
                $this->count++;

                $reverse = new Distance();
                $reverse->setOrigin($entity->getDestination());
                $reverse->setDestination($entity->getOrigin());
                $reverse->set($entity->getMiles());
                $reverse->isReverse = true;

                $em = $eventArgs->getEntityManager();
                $em->persist($reverse);
                $em->flush();
            }
        }
    }
}

Output:

Entity created at 1433168310.8787 is not reverse
Entity created at 1433168310.9073 is reverse
Entity created at 1433168310.8787 is not reverse
Entity created at 1433168310.9078 is reverse
Entity created at 1433168310.8787 is not reverse
Entity created at 1433168310.908 is reverse
Entity created at 1433168310.8787 is not reverse
Entity created at 1433168310.9084 is reverse
Entity created at 1433168310.8787 is not reverse
Entity created at 1433168310.9087 is reverse
Entity created at 1433168310.8787 is not reverse

It's like the original entity (creation time ending with 8787) was persisted an infinite number of times.

Just in case, if I remove the call to $em->flush, I correctly get the following output:

Entity created at 1433167824.2552 is not reverse
Entity created at 1433167824.2947 is reverse

but then an exception saying that no parameters were bound to the insert query. Which is confirmed by Symfony's profiler:

INSERT INTO Distance (
    miles, origin_id, destination_id
) 
VALUES 
(?, ?, ?)
Parameters: { }

I'd like to understand why my listener doesn't work as I expect, and how to fix it.


As requested, here's some more code. Everything comes from a Place Form where, apart from entering the Place name, I can add/delete/edit a collection of distances to other Places.

// PlaceController::updateAction
public function updateAction(Request $request, $id)
{
    $em = $this->getDoctrine()->getManager();

    $entity = $em->getRepository('MyBundle:Place')->find($id);
    if (! $entity) {
        throw $this->createNotFoundException('Unable to find Place entity.');
    }

    $deleteForm = $this->createDeleteForm($id);
    $editForm = $this->createForm(new PlaceType(), $entity, array(
        'action' => $this->generateUrl('update_place', array('id' => $entity->getId())),
        'method' => 'PUT'
    ));
    $editForm->add('submit', 'submit', array('label' => 'panel.button.save'));

    $editForm->handleRequest($request);

    if ($editForm->isValid()) {
        $em->flush();

    return array(
        'entity' => $entity,
        'form' => $editForm->createView(),
        'delete_form' => $deleteForm->createView(),
    );
}

// PlaceType::buildForm
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $Place = $builder->getData();

    $builder
        ->add(
            'name',
            'text',
            [
                'label' => 'object.place.name'
            ]
        )
        ->add(
            'distancesTo',
            'collection',
            [
                'label' => 'object.place.distance.plural',
                'type' => new DistanceType(),
                'by_reference' => false,
                'allow_add' => true,
                'allow_delete' => true,
                'options' => [
                    'required' => false,
                    'origin' => $Place->getId() ? $Place : null
                ]
            ]
        );
}

回答1:

You should not use $em->flush() inside prePersist, it is restricted by Doctrine: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#reference-events-implementing-listeners

There is information about preUpdate, but same situation (loop) is applied for prePesist call

9.6.6. preUpdate

PreUpdate is the most restrictive to use event, since it is called right before an update statement is called for an entity inside the EntityManager#flush() method. Changes to associations of the updated entity are never allowed in this >event, since Doctrine cannot guarantee to correctly handle referential ?integrity at this point of the flush operation.

Similar sitaution is described here preUpdate() siblings manage into tree: how to break ->persist() recursion?

So you can also do similar way: create custom event, create custom event subscriber where you will create reverse entity and dispatch that event subscriber at the controller action.