瀏覽代碼

[tree] delay node insertion if parent was not inserted in tree yet, references #187

In some cases commit order calculator of Doctrine2 could change the
order of insertions, which brokes tree updates.
Issue fixed by delaying node update, if parent is new node and was not
processed by nested tree strategy yet
Illya Klymov 13 年之前
父節點
當前提交
1e5d627cd8

+ 22 - 0
lib/Gedmo/Tree/Strategy/ORM/Nested.php

@@ -72,6 +72,13 @@ class Nested implements Strategy
      */
     private $nodePositions = array();
 
+    /**
+     * Stores a list of delayed nodes for correct order of updates
+     *
+     * @var arrau
+     */
+    private $delayedNodes = array();
+
     /**
      * {@inheritdoc}
      */
@@ -314,8 +321,18 @@ class Nested implements Strategy
             $wrappedParent = AbstractWrapper::wrap($parent, $em);
 
             $parentRootId = isset($config['root']) ? $wrappedParent->getPropertyValue($config['root']) : null;
+            $parentOid = spl_object_hash($parent);
             $parentLeft = $wrappedParent->getPropertyValue($config['left']);
             $parentRight = $wrappedParent->getPropertyValue($config['right']);
+            if (empty($parentLeft) && empty($parentRight)) {
+                // parent node is a new node, but wasn't processed yet (due to Doctrine commit order calculator redordering)
+                // We delay processing of node to the moment parent node will be processed
+                if (!isset($this->delayedNodes[$parentOid])) {
+                    $this->delayedNodes[$parentOid] = array();
+                }
+                $this->delayedNodes[$parentOid][] = array('node' => $node, 'position' => $position);
+                return;
+            }
             if (!$isNewNode && $rootId === $parentRootId && $parentLeft >= $left && $parentRight <= $right) {
                 throw new UnexpectedValueException("Cannot set child as parent to node: {$nodeId}");
             }
@@ -417,6 +434,11 @@ class Nested implements Strategy
             $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['left'], $left + $diff);
             $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['right'], $right + $diff);
         }
+        if (isset($this->delayedNodes[$oid])) {
+            foreach($this->delayedNodes[$oid] as $nodeData) {
+                $this->updateNode($em, $nodeData['node'], $node, $nodeData['position']);
+            }
+        }
     }
 
     /**

+ 10 - 0
tests/Gedmo/Tree/Fixture/Genealogy/Man.php

@@ -0,0 +1,10 @@
+<?php
+namespace Tree\Fixture\Genealogy;
+
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
+ */
+class Man extends Person {
+}

+ 98 - 0
tests/Gedmo/Tree/Fixture/Genealogy/Person.php

@@ -0,0 +1,98 @@
+<?php
+namespace Tree\Fixture\Genealogy;
+
+use Doctrine\Common\Collections\ArrayCollection;
+use Gedmo\Mapping\Annotation as Gedmo;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
+ * @ORM\Table(name="genealogy")
+ * @ORM\InheritanceType("SINGLE_TABLE")
+ * @ORM\DiscriminatorColumn(name="discr", type="string")
+ * @ORM\DiscriminatorMap({"man" = "Man", "woman" = "Woman"})
+ * @Gedmo\Tree(type="nested")
+ */
+abstract class Person {
+
+  /**
+   * @ORM\Column(name="id", type="integer")
+   * @ORM\Id
+   * @ORM\GeneratedValue
+   * @var int
+   */
+  private $id;
+
+  /**
+   * @Gedmo\TreeParent
+   * @ORM\ManyToOne(targetEntity="Person", inversedBy="children")
+   * @var Person
+   */
+  private $parent;
+
+  /**
+   * @ORM\OneToMany(targetEntity="Person", mappedBy="parent")
+   * @var Doctrine\Common\Collections\ArrayCollection
+   */
+  protected $children;
+
+  /**
+   * @Gedmo\TreeLeft
+   * @ORM\Column(name="lft", type="integer")
+   */
+  private $lft;
+
+  /**
+   * @Gedmo\TreeRight
+   * @ORM\Column(name="rgt", type="integer")
+   */
+  private $rgt;
+
+  /**
+   * @Gedmo\TreeLevel
+   * @ORM\Column(name="lvl", type="integer")
+   */
+  private $lvl;
+
+  /**
+   * @ORM\Column(name="name", type="string", length=255, nullable=false)
+   * @var string
+   */
+  private $name;
+
+  /**
+   * @param string $name
+   */
+  public function __construct($name) {
+    $this->name = $name;
+    $this->children = new ArrayCollection();
+  }
+
+  /**
+   * @param Person $parent
+   * @return Person
+   */
+  public function setParent(Person $parent) {
+    $this->parent = $parent;
+    return $this;
+  }
+
+  public function getName() {
+    return $this->name;
+  }
+
+  public function getLeft()
+  {
+      return $this->lft;
+  }
+
+  public function getRight()
+  {
+      return $this->rgt;
+  }
+
+  public function getLevel()
+  {
+      return $this->lvl;
+  }
+}

+ 10 - 0
tests/Gedmo/Tree/Fixture/Genealogy/Woman.php

@@ -0,0 +1,10 @@
+<?php
+namespace Tree\Fixture\Genealogy;
+
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
+ */
+class Woman extends Person {
+}

+ 97 - 0
tests/Gedmo/Tree/InMemoryUpdatesWithInheritanceTest.php

@@ -0,0 +1,97 @@
+<?php
+
+namespace Gedmo\Tree;
+
+use Doctrine\Common\EventManager;
+use Tool\BaseTestCaseORM;
+use Tree\Fixture\Genealogy\Man;
+use Tree\Fixture\Genealogy\Woman;
+
+/**
+ * Additional tests for tree inheritance and in-memory updates
+ *
+ * @author Illya Klymov <xanf@xanf.me>
+ * @package Gedmo.Tree
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class InMemoryUpdatesWithInheritance extends BaseTestCaseORM
+{
+
+    const PERSON = "Tree\\Fixture\\Genealogy\\Person";
+    const MAN = "Tree\\Fixture\\Genealogy\\Man";
+    const WOMAN = "Tree\\Fixture\\Genealogy\\Woman";
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $evm = new EventManager;
+        $evm->addEventSubscriber(new TreeListener);
+
+        $this->getMockSqliteEntityManager($evm);
+    }
+
+    public function testInMemoryTreeInsertsWithInheritance()
+    {
+        $nodes = array();
+
+        $man1 = new Man('Root - Man1');
+        $this->em->persist($man1);
+
+        $woman1 = new Woman('Level 1 - Woman1');
+        $this->em->persist($woman1);
+        $woman1->setParent($man1);
+
+        $man2 = new Man('Level 2 - Man2');
+        $this->em->persist($man2);
+        $man2->setParent($woman1);
+
+        $woman2 = new Woman('Level 3 - Woman2');
+        $this->em->persist($woman2);
+        $woman2->setParent($man2);
+
+        $this->em->flush();
+
+        $this->em->refresh($man1);
+        $left = $man1->getLeft();
+        $right = $man1->getRight();
+        $level = $man1->getLevel();
+        $this->assertEquals(1, $left);
+        $this->assertEquals(8, $right);
+        $this->assertEquals(0, $level);
+
+        $this->em->refresh($woman1);
+        $left = $woman1->getLeft();
+        $right = $woman1->getRight();
+        $level = $woman1->getLevel();
+        $this->assertEquals(2, $left);
+        $this->assertEquals(7, $right);
+        $this->assertEquals(1, $level);
+
+        $this->em->refresh($man2);
+        $left = $man2->getLeft();
+        $right = $man2->getRight();
+        $level = $man2->getLevel();
+        $this->assertEquals(3, $left);
+        $this->assertEquals(6, $right);
+        $this->assertEquals(2, $level);
+
+        $this->em->refresh($woman2);
+        $left = $woman2->getLeft();
+        $right = $woman2->getRight();
+        $level = $woman2->getLevel();
+        $this->assertEquals(4, $left);
+        $this->assertEquals(5, $right);
+        $this->assertEquals(3, $level);
+    }
+
+    protected function getUsedEntityFixtures()
+    {
+        return array(
+            self::PERSON,
+            self::MAN,
+            self::WOMAN
+        );
+    }
+}