Sfoglia il codice sorgente

[Tree - Closure Table] Added support for a ChildCount field (EXPERIMENTAL)

Gustavo Adrian 14 anni fa
parent
commit
071392073c

+ 1 - 0
lib/Gedmo/Tree/Mapping/Annotations.php

@@ -25,6 +25,7 @@ final class TreeRight extends Annotation {}
 final class TreeParent extends Annotation {}
 final class TreeLevel extends Annotation {}
 final class TreeRoot extends Annotation {}
+final class TreeChildCount extends Annotation {}
 final class TreeClosure extends Annotation
 {
     public $class; 

+ 16 - 0
lib/Gedmo/Tree/Mapping/Driver/Annotation.php

@@ -55,6 +55,11 @@ class Annotation implements Driver
      * Annotation to specify closure tree class
      */
     const ANNOTATION_CLOSURE = 'Gedmo\Tree\Mapping\TreeClosure';
+	
+	/**
+     * Annotation to mark field as child count
+     */
+    const ANNOTATION_CHILD_COUNT = 'Gedmo\Tree\Mapping\TreeChildCount';
     
     /**
      * List of types which are valid for tree fields
@@ -176,6 +181,17 @@ class Annotation implements Driver
                 }
                 $config['level'] = $field;
             }
+            // child count
+            if ($childCount = $reader->getPropertyAnnotation($property, self::ANNOTATION_CHILD_COUNT)) {
+               $field = $property->getName();
+               if (!$meta->hasField($field)) {
+                    throw new InvalidMappingException("Unable to find 'childCount' - [{$field}] as mapped property in entity - {$meta->name}");
+                }
+                if (!$this->isValidField($meta, $field)) {
+                    throw new InvalidMappingException("Tree childCount field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
+                }
+                $config['childCount'] = $field;
+            }
         }
     }
     

+ 65 - 22
lib/Gedmo/Tree/Strategy/ORM/Closure.php

@@ -75,11 +75,17 @@ class Closure implements Strategy
      */
     public function processPostPersist($em, $entity)
     {        
-        if (count($this->pendingChildNodeInserts)) 
-		{
-            while ($entity = array_shift($this->pendingChildNodeInserts)) 
+        if (count($this->pendingChildNodeInserts)) {
+            while ($e = array_shift($this->pendingChildNodeInserts)) 
             {
-                $this->insertNode($em, $entity);
+                $this->insertNode($em, $e);
+            }
+            
+            // If "childCount" property is in the schema, we recalculate child count of all entities
+            $meta = $em->getClassMetadata(get_class($entity));
+            $config = $this->listener->getConfiguration($em, $meta->name);
+            if (isset($config['childCount'])) {
+                $this->recalculateChildCountForEntities($em, get_class( $entity ));
             }
         }
     }
@@ -89,16 +95,20 @@ class Closure implements Strategy
         $meta = $em->getClassMetadata(get_class($entity));
         $config = $this->listener->getConfiguration($em, $meta->name);
         $identifier = $meta->getSingleIdentifierFieldName();
-        $id = $meta->getReflectionProperty($identifier)->getValue($entity);
+        $id = $this->extractIdentifier( $em, $entity );
         $closureMeta = $em->getClassMetadata($config['closure']);
+        $entityTable = $meta->getTableName();
+        $closureTable = $closureMeta->getTableName();
         $entries = array();
-		
+		$childrenIDs = array();
+        $ancestorsIDs = array();
+        
 		// If node has children it means it already has a self referencing row, so we skip its insertion
 		if ($addNodeChildrenToAncestors === false) {
             $entries[] = array(
-                'ancestor'		=> $id,
-                'descendant' 	=> $id,
-                'depth' 		=> 0
+                'ancestor' => $id,
+                'descendant' => $id,
+                'depth' => 0
             );
         }
 		
@@ -109,35 +119,36 @@ class Closure implements Strategy
             $dql = "SELECT c.ancestor, c.depth FROM {$closureMeta->name} c";
             $dql .= " WHERE c.descendant = {$parentId}";
             $ancestors = $em->createQuery($dql)->getArrayResult();
-            //echo count($ancestors);
+            
             foreach ($ancestors as $ancestor) {
                 $entries[] = array(
                     'ancestor' => $ancestor['ancestor'],
                     'descendant' => $id,
                     'depth' => $ancestor['depth'] + 1
                 );
+                $ancestorsIDs[] = $ancestor['ancestor'];
 				
                 if ($addNodeChildrenToAncestors === true) {
-                    $dql 		= "SELECT c.descendant, c.depth FROM {$closureMeta->name} c";
-                    $dql 		.= " WHERE c.ancestor = {$id} AND c.ancestor != c.descendant";
-                    $children 	= $em->createQuery($dql)
+                    $dql = "SELECT c.descendant, c.depth FROM {$closureMeta->name} c";
+                    $dql .= " WHERE c.ancestor = {$id} AND c.ancestor != c.descendant";
+                    $children = $em->createQuery($dql)
                         ->getArrayResult();
                     
                     foreach ($children as $child)
                     {
                         $entries[] = array(
-                            'ancestor'		=> $ancestor['ancestor'],
-                            'descendant'	=> $child['descendant'],
-                            'depth'			=> $child['depth'] + 1
+                            'ancestor' => $ancestor['ancestor'],
+                            'descendant' => $child['descendant'],
+                            'depth' => $child['depth'] + 1
                         );
+                        $childrenIDs[] = $child['descendant'];
                     }
                 }
-            }	
+            } 
         }
         
-        $table = $closureMeta->getTableName();
         foreach ($entries as $closure) {
-            if (!$em->getConnection()->insert($table, $closure)) {
+            if (!$em->getConnection()->insert($closureTable, $closure)) {
                 throw new \Gedmo\Exception\RuntimeException('Failed to insert new Closure record');
             }
         }
@@ -156,6 +167,11 @@ class Closure implements Strategy
         if (array_key_exists($config['parent'], $changeSet)) {
             $this->updateNode($em, $entity, $changeSet[$config['parent']]);
         }
+        
+        // If "childCount" property is in the schema, we recalculate child count of all entities
+        if (isset($config['childCount'])) {
+            $this->recalculateChildCountForEntities($em, get_class( $entity ));
+        }
     }
     
     public function updateNode(EntityManager $em, $entity, array $change)
@@ -167,8 +183,7 @@ class Closure implements Strategy
         $nodeId = $this->extractIdentifier($em, $entity);
         $table = $closureMeta->getTableName();
 		
-        if ($oldParent) 
-		{
+        if ($oldParent) {
             $this->removeClosurePathsOfNodeID($em, $table, $nodeId);
             
             $this->insertNode($em, $entity, true);
@@ -184,6 +199,14 @@ class Closure implements Strategy
     public function processScheduledDelete($em, $entity)
     {
         $this->removeNode($em, $entity);
+        
+        // If "childCount" property is in the schema, we recalculate child count of all entities
+        $meta = $em->getClassMetadata(get_class($entity));
+        $config = $this->listener->getConfiguration($em, $meta->name);
+        
+        if (isset($config['childCount'])) {
+            $this->recalculateChildCountForEntities($em, get_class( $entity ));
+        }
     }
 	
     public function removeNode(EntityManager $em, $entity, $maintainSelfReferencingRow = false, $maintainSelfReferencingRowOfChildren = false)
@@ -191,8 +214,9 @@ class Closure implements Strategy
         $meta = $em->getClassMetadata(get_class($entity));
         $config = $this->listener->getConfiguration($em, $meta->name);
         $closureMeta = $em->getClassMetadata($config['closure']);
+        $id = $this->extractIdentifier( $em, $entity );
         
-        $this->removeClosurePathsOfNodeID($em, $closureMeta->getTableName(), $entity->getId(), $maintainSelfReferencingRow, $maintainSelfReferencingRowOfChildren);
+        $this->removeClosurePathsOfNodeID($em, $closureMeta->getTableName(), $id, $maintainSelfReferencingRow, $maintainSelfReferencingRowOfChildren);
 	}
     
 	public function removeClosurePathsOfNodeID(EntityManager $em, $table, $nodeId, $maintainSelfReferencingRow = true, $maintainSelfReferencingRowOfChildren = true)
@@ -220,6 +244,25 @@ class Closure implements Strategy
         }
     }
     
+    public function recalculateChildCountForEntities($em, $entityClass)
+    {
+        $meta = $em->getClassMetadata($entityClass);
+        $config = $this->listener->getConfiguration($em, $meta->name);
+        $entityIdentifierField = $meta->getIdentifierColumnNames();
+        $entityIdentifierField = $entityIdentifierField[ 0 ];
+        $childCountField = $config['childCount'];
+        $closureMeta = $em->getClassMetadata($config['closure']);
+        $entityTable = $meta->getTableName();
+        $closureTable = $closureMeta->getTableName();
+        
+        $subquery = "( SELECT COUNT( c2.descendant ) FROM {$closureTable} c2 WHERE c2.ancestor = c1.{$entityIdentifierField} AND c2.ancestor != c2.descendant )";
+        $sql = "UPDATE {$entityTable} c1 SET c1.{$childCountField} = {$subquery}";
+        
+        if (!$em->getConnection()->executeQuery($sql)) {
+            throw new \Gedmo\Exception\RuntimeException('Failed to update child count field of entities');
+        }
+    }
+    
     private function extractIdentifier($em, $entity, $single = true)
     {
         if ($entity instanceof Proxy) {

+ 44 - 8
tests/Gedmo/Tree/ClosureTreeTest.php

@@ -82,12 +82,6 @@ class ClosureTreeTest extends \PHPUnit_Framework_TestCase
         $this->populate();
     }
     
-    public function tearDown()
-    {
-        $this->analyzer->dumpResult();
-        $this->em->clear();
-    }
-    
     public function test_insertNodes_verifyClosurePaths()
     {        
         // We check the inserted nodes fields from the closure table
@@ -149,8 +143,25 @@ class ClosureTreeTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals( $rows[ 11 ][ 'descendant' ], $this->potatoes->getId() );
         $this->assertEquals( $rows[ 11 ][ 'depth' ], 0 );
     }
+    
+    public function test_childcount_afterInsertingNodesChildCountIsCalculated()
+    {
+        $this->em->refresh( $this->food );
+        $this->em->refresh( $this->sports );
+        $this->em->refresh( $this->fruits );
+        $this->em->refresh( $this->vegitables );
+        $this->em->refresh( $this->carrots );
+        $this->em->refresh( $this->potatoes );
+        
+        $this->assertEquals( $this->food->getChildCount(), 4 );
+        $this->assertEquals( $this->sports->getChildCount(), 0 );
+        $this->assertEquals( $this->fruits->getChildCount(), 0 );
+        $this->assertEquals( $this->vegitables->getChildCount(), 2 );
+        $this->assertEquals( $this->carrots->getChildCount(), 0 );
+        $this->assertEquals( $this->potatoes->getChildCount(), 0 );
+    }
 	
-    public function test_updateNodes_moveASubtreeAndVerifyTreeClosurePaths()
+    public function test_updateNodes_moveASubtreeVerifyTreeClosurePathsAndVerifyChildCountField()
     {
         // We change a subtree's location
         $vegitables = $this->em->getRepository( self::TEST_ENTITY_CLASS )
@@ -220,6 +231,22 @@ class ClosureTreeTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals( $rows[ 11 ][ 'ancestor' ], $this->potatoes->getId() );
         $this->assertEquals( $rows[ 11 ][ 'descendant' ], $this->potatoes->getId() );
         $this->assertEquals( $rows[ 11 ][ 'depth' ], 0 );
+        
+        
+        // We check the childCount field
+        $this->em->refresh( $this->food );
+        $this->em->refresh( $this->sports );
+        $this->em->refresh( $this->fruits );
+        $this->em->refresh( $this->vegitables );
+        $this->em->refresh( $this->carrots );
+        $this->em->refresh( $this->potatoes );
+        
+        $this->assertEquals( $this->food->getChildCount(), 1 );
+        $this->assertEquals( $this->sports->getChildCount(), 3 );
+        $this->assertEquals( $this->fruits->getChildCount(), 0 );
+        $this->assertEquals( $this->vegitables->getChildCount(), 2 );
+        $this->assertEquals( $this->carrots->getChildCount(), 0 );
+        $this->assertEquals( $this->potatoes->getChildCount(), 0 );
     }
     
     public function test_removeNode_removesClosurePathsOfNodeAndVerifyTree()
@@ -253,6 +280,16 @@ class ClosureTreeTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals( $rows[ 3 ][ 'ancestor' ], $this->fruits->getId() );
         $this->assertEquals( $rows[ 3 ][ 'descendant' ], $this->fruits->getId() );
         $this->assertEquals( $rows[ 3 ][ 'depth' ], 0 );
+        
+        
+        // We check the childCount field
+        $this->em->refresh( $this->food );
+        $this->em->refresh( $this->sports );
+        $this->em->refresh( $this->fruits );
+        
+        $this->assertEquals( $this->food->getChildCount(), 1 );
+        $this->assertEquals( $this->sports->getChildCount(), 0 );
+        $this->assertEquals( $this->fruits->getChildCount(), 0 );
     }
     
     private function populate()
@@ -293,6 +330,5 @@ class ClosureTreeTest extends \PHPUnit_Framework_TestCase
         $this->em->persist($this->potatoes);
         
         $this->em->flush();
-        $this->em->clear();
     }
 }

+ 11 - 0
tests/Gedmo/Tree/Fixture/Closure/Category.php

@@ -32,6 +32,12 @@ class Category
      * @OneToMany(targetEntity="Category", mappedBy="parent", cascade={"persist"})
      */
     private $children;
+    
+    /**
+     * @gedmo:TreeChildCount
+     * @Column(type="integer", nullable="true")
+     */
+    private $childCount;
 
     public function getId()
     {
@@ -57,4 +63,9 @@ class Category
     {
         return $this->parent;    
     }
+    
+    public function getChildCount()
+    {
+        return $this->childCount;
+    }
 }