preUpdate() siblings manage into tree: how to brea

2019-04-13 05:18发布

Let's say I've got an entity like this

class FooEntity
{
  $id;

  //foreign key with FooEntity itself
  $parent_id;

  //if no parent level =1, if have a parent without parent itself = 2 and so on...
  $level;

  //sorting index is relative to level
  $sorting_index
}

Now I would like on delete and on edit to change level and sorting_index of this entity.

So I've decided to take advantage of Doctrine2 EntityListeners and I've done something similar to

class FooListener
{
  public function preUpdate(Foo $entity, LifecycleEventArgs $args)
    {
        $em = $args->getEntityManager();
        $this->handleEntityOrdering($entity, $em);
    }

    public function preRemove(Foo $entity, LifecycleEventArgs $args)
    {
        $level = $entity->getLevel();
        $cur_sorting_index = $entity->getSortingIndex();
        $em = $args->getEntityManager();
        $this->handleSiblingOrdering($level, $cur_sorting_index, $em);
    }

    private function handleEntityOrdering($entity, $em)
    {
        error_log('entity to_update_category stop flag: '.$entity->getStopEventPropagationStatus());
        error_log('entity splobj: '.spl_object_hash($entity));
        //code to calculate new sorting_index and level for this entity (omitted)
        $this->handleSiblingOrdering($old_level, $old_sorting_index, $em);
        }
    }

    private function handleSiblingOrdering($level, $cur_sorting_index, $em)
    {   
        $to_update_foos = //retrieve from db all siblings that needs an update
        //some code to update sibling ordering (omitted)
        foreach ($to_update_foos as $to_update_foo)
        {
            $em->persist($to_update_foo);
        }
        $em->flush();
    }
}

The problem here is pretty clear: if I persist a Foo entity, preUpdate() (into handleSiblingOrdering function) trigger is raised and this cause an infinite loop.

My first idea was to insert a special variable inside my entity to prevent this loop: when I started a sibling update, that variable is setted and before executing the update code is checked. This works like a charm for preRemove() but not for preUpdate().
If you notice I'm logging spl_obj_hash to understand this behaviour. With a big surprise I can see that obj passed to preUpdate() after a preRemove() is the same (so setting a "status flag" is a fine) but the object passed to preUpdate() after a preUpdate() isn't the same.

So ...

First question

Someone could point me in the right direction to manage this situation?

Second question

Why doctrine needs to generate different objects if two similar events are raised?

2条回答
叛逆
2楼-- · 2019-04-13 05:44

You are doing wrong approach by calling $em->flush() inside preUpdate, I even can say restricted by Doctrine action: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#reference-events-implementing-listeners

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.

查看更多
小情绪 Triste *
3楼-- · 2019-04-13 05:46

I've founded a workaround

Best approach to this problem seem to create a custom EventSubscriber with a custom Event dispatched programmatically into controller update action.
That way I can "break" the loop and having a working code.

Just to make this answer complete I will report some snippet of code just to clarify che concept

Create custom events for your bundle

//src/path/to/your/bundle/YourBundleNameEvents.php 
final class YourBundleNameEvents
{
    const FOO_EVENT_UPDATE = 'bundle_name.foo.update';
}

this is a special class that will not do anything but provide some custom events for our bundle

Create a custom event for foo update

//src/path/to/your/bundle/Event/FooUpdateEvent
class FooUpdateEvent
{
  //this is the class that will be dispatched so add properties useful for your own logic. In my example two properties could be $level and $sorting_index. This values are setted BEFORE dispatch the event
}

Create a custom event subscriber

//src/path/to/your/bundle/EventListener/FooSubscriber
class FooSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(YourBundleNameEvents::FooUpdate => 'handleSiblingsOrdering');
    }

    public function handleSiblingsOrdering(FooUpdateEvent $event)
    {
        //I can retrieve there, from $event, all data I setted into event itself. Now I can run all my own logic code to re-order siblings
    }
}

Register your Subscriber as a service

//app/config/config.yml

services:
your_bundlename.foo_listener:
        class: Your\Bundle\Name\EventListener\FooListener
        tags:
            - { name: kernel.event_subscriber }

Create and dispatch events into controller

//src/path/to/your/bundle/Controller/FooController
class FooController extends Controller
{
    public function updateAction()
    {
        //some code here
        $dispatcher = $this->get('event_dispatcher');
        $foo_event = new FooEvent();
        $foo_event->setLevel($level); //just an example
        $foo_event->setOrderingIndex($ordering_index); //just an examle

        $dispatcher->dispatch(YourBundleNameEvents::FooUpdate, $foo_event);
    }
}

Alternative solution

Of course above solution is the best one but, if you have a property mapped into db that could be used as a flag, you could access it directly from LifecycleEventArgs of preUpdate() event by calling

$event->getNewValue('flag_name'); //$event is an object of LifecycleEventArgs type

By using that flag we could check for changes and stop the propagation

查看更多
登录 后发表回答