Skip to content

Commit 7d0201f

Browse files
committed
[Tree] Allow hydrating subtree in tree object hydrator
1 parent 562c8c9 commit 7d0201f

File tree

2 files changed

+155
-16
lines changed

2 files changed

+155
-16
lines changed

lib/Gedmo/Tree/Hydrator/ORM/TreeObjectHydrator.php

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Doctrine\Common\Collections\ArrayCollection;
77
use Doctrine\ORM\EntityManagerInterface;
88
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
9+
use Doctrine\ORM\Proxy\Proxy;
910
use Gedmo\Tool\Wrapper\EntityWrapper;
1011
use Gedmo\Tree\TreeListener;
1112

@@ -58,14 +59,13 @@ protected function hydrateAllData()
5859
$this->parentField = $this->getParentField();
5960
$this->childrenField = $this->getChildrenField($entityClass);
6061

62+
6163
$childrenHashmap = $this->buildChildrenHashmap($data);
6264
$this->populateChildrenArray($data, $childrenHashmap);
6365

64-
// Only return root elements
66+
// Only return root elements or elements who's parents haven't been fetched
6567
// The sub-nodes will be accessible via the `children` property
66-
return isset($childrenHashmap[null])
67-
? $childrenHashmap[null]
68-
: array();
68+
return $this->getRootNodes($data);
6969
}
7070

7171
/**
@@ -83,13 +83,11 @@ protected function buildChildrenHashmap($nodes)
8383
$r = array();
8484

8585
foreach ($nodes as $node) {
86-
$wrapper = new EntityWrapper($node, $this->_em);
87-
$parentProxy = $wrapper->getPropertyValue($this->config['parent']);
86+
$parentProxy = $this->getPropertyValue($node, $this->config['parent']);
8887
$parentId = null;
8988

9089
if ($parentProxy !== null) {
91-
$parentWrapper = new EntityWrapper($parentProxy, $this->_em);
92-
$parentId = $parentWrapper->getPropertyValue($this->idField);
90+
$parentId = $this->getPropertyValue($parentProxy, $this->idField);
9391
}
9492

9593
$r[$parentId][] = $node;
@@ -105,13 +103,12 @@ protected function buildChildrenHashmap($nodes)
105103
protected function populateChildrenArray($nodes, $childrenHashmap)
106104
{
107105
foreach ($nodes as $node) {
108-
$wrapper = new EntityWrapper($node, $this->_em);
109-
$nodeId = $wrapper->getPropertyValue($this->idField);
110-
$childrenCollection = $wrapper->getPropertyValue($this->childrenField);
106+
$nodeId = $this->getPropertyValue($node, $this->idField);
107+
$childrenCollection = $this->getPropertyValue($node, $this->childrenField);
111108

112109
if ($childrenCollection === null) {
113110
$childrenCollection = new ArrayCollection();
114-
$wrapper->setPropertyValue($this->childrenField, $childrenCollection);
111+
$this->setPropertyValue($node, $this->childrenField, $childrenCollection);
115112
}
116113

117114
// Mark all children collections as initialized to avoid select queries
@@ -131,6 +128,53 @@ protected function populateChildrenArray($nodes, $childrenHashmap)
131128
}
132129
}
133130

131+
/**
132+
* @param array $nodes
133+
* @return array
134+
*/
135+
protected function getRootNodes($nodes)
136+
{
137+
$idHashmap = $this->buildIdHashmap($nodes);
138+
$rootNodes = array();
139+
140+
foreach ($nodes as $node) {
141+
$parentProxy = $this->getPropertyValue($node, $this->config['parent']);
142+
$parentId = null;
143+
144+
if ($parentProxy !== null) {
145+
$parentId = $this->getPropertyValue($parentProxy, $this->idField);
146+
}
147+
148+
if ($parentId === null || !key_exists($parentId, $idHashmap)) {
149+
$rootNodes[] = $node;
150+
}
151+
}
152+
153+
return $rootNodes;
154+
}
155+
156+
/**
157+
* Creates a hashmap of all nodes returned in the query
158+
*
159+
* ```
160+
* [node1.id => true, node2.id => true, ...]
161+
* ```
162+
*
163+
* @param array $nodes
164+
* @return array
165+
*/
166+
protected function buildIdHashmap(array $nodes)
167+
{
168+
$ids = array();
169+
170+
foreach ($nodes as $node) {
171+
$id = $this->getPropertyValue($node, $this->idField);
172+
$ids[$id] = true;
173+
}
174+
175+
return $ids;
176+
}
177+
134178
/**
135179
* @return string
136180
*/
@@ -206,4 +250,16 @@ protected function getEntityClassFromHydratedData($data)
206250
$firstMappedEntity = $firstMappedEntity[0];
207251
return get_class($firstMappedEntity);
208252
}
253+
254+
protected function getPropertyValue($object, $property)
255+
{
256+
$meta = $this->_em->getClassMetadata(get_class($object));
257+
return $meta->getReflectionProperty($property)->getValue($object);
258+
}
259+
260+
public function setPropertyValue($object, $property, $value)
261+
{
262+
$meta = $this->_em->getClassMetadata(get_class($object));
263+
$meta->getReflectionProperty($property)->setValue($object, $value);
264+
}
209265
}

tests/Gedmo/Tree/TreeObjectHydratorTest.php

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Doctrine\Common\EventManager;
66
use Doctrine\DBAL\Logging\DebugStack;
77
use Doctrine\ORM\Query;
8+
use Gedmo\Tree\Entity\Repository\NestedTreeRepository;
89
use Tool\BaseTestCaseORM;
910
use Tree\Fixture\RootCategory;
1011

@@ -35,13 +36,15 @@ protected function setUp()
3536
public function testFullTreeHydration()
3637
{
3738
$this->populate();
39+
$this->em->clear();
3840

3941
$stack = new DebugStack();
4042
$this->em->getConfiguration()->setSQLLogger($stack);
4143

4244
$repo = $this->em->getRepository(self::ROOT_CATEGORY);
4345

4446
$result = $repo->createQueryBuilder('node')
47+
->orderBy('node.lft', 'ASC')
4548
->getQuery()
4649
->setHint(Query::HINT_INCLUDE_META_COLUMNS, true)
4750
->getResult('tree');
@@ -80,6 +83,86 @@ public function testFullTreeHydration()
8083
$this->assertEquals(count($stack->queries), 1);
8184
}
8285

86+
public function testPartialTreeHydration()
87+
{
88+
$this->populate();
89+
$this->em->clear();
90+
91+
$stack = new DebugStack();
92+
$this->em->getConfiguration()->setSQLLogger($stack);
93+
94+
/** @var NestedTreeRepository $repo */
95+
$repo = $this->em->getRepository(self::ROOT_CATEGORY);
96+
97+
$fruits = $repo->findOneBy(array('title' => 'Fruits'));
98+
99+
$result = $repo->getChildrenQuery($fruits, false, null, 'ASC', true)
100+
->setHint(Query::HINT_INCLUDE_META_COLUMNS, true)
101+
->getResult('tree');
102+
103+
$this->assertEquals(count($result), 1);
104+
105+
$fruits = $result[0];
106+
$this->assertEquals($fruits->getTitle(), 'Fruits');
107+
$this->assertEquals(count($fruits->getChildren()), 2);
108+
109+
$oranges = $fruits->getChildren()->get(0);
110+
$this->assertEquals($oranges->getTitle(), 'Oranges');
111+
$this->assertEquals(count($oranges->getChildren()), 0);
112+
113+
$citrons = $fruits->getChildren()->get(1);
114+
$this->assertEquals($citrons->getTitle(), 'Citrons');
115+
$this->assertEquals(count($citrons->getChildren()), 0);
116+
117+
$this->assertEquals(count($stack->queries), 2);
118+
}
119+
120+
public function testMultipleRootNodesTreeHydration()
121+
{
122+
$this->populate();
123+
$this->em->clear();
124+
125+
$stack = new DebugStack();
126+
$this->em->getConfiguration()->setSQLLogger($stack);
127+
128+
/** @var NestedTreeRepository $repo */
129+
$repo = $this->em->getRepository(self::ROOT_CATEGORY);
130+
131+
$food = $repo->findOneBy(array('title' => 'Food'));
132+
133+
$result = $repo->getChildrenQuery($food)
134+
->setHint(Query::HINT_INCLUDE_META_COLUMNS, true)
135+
->getResult('tree');
136+
137+
$this->assertEquals(count($result), 4);
138+
139+
$fruits = $result[0];
140+
$this->assertEquals($fruits->getTitle(), 'Fruits');
141+
$this->assertEquals(count($fruits->getChildren()), 2);
142+
143+
$vegetables = $result[1];
144+
$this->assertEquals($vegetables->getTitle(), 'Vegetables');
145+
$this->assertEquals(count($vegetables->getChildren()), 0);
146+
147+
$milk = $result[2];
148+
$this->assertEquals($milk->getTitle(), 'Milk');
149+
$this->assertEquals(count($milk->getChildren()), 0);
150+
151+
$meat = $result[3];
152+
$this->assertEquals($meat->getTitle(), 'Meat');
153+
$this->assertEquals(count($meat->getChildren()), 0);
154+
155+
$oranges = $fruits->getChildren()->get(0);
156+
$this->assertEquals($oranges->getTitle(), 'Oranges');
157+
$this->assertEquals(count($oranges->getChildren()), 0);
158+
159+
$citrons = $fruits->getChildren()->get(1);
160+
$this->assertEquals($citrons->getTitle(), 'Citrons');
161+
$this->assertEquals(count($citrons->getChildren()), 0);
162+
163+
$this->assertEquals(count($stack->queries), 2);
164+
}
165+
83166
private function populate()
84167
{
85168
$repo = $this->em->getRepository(self::ROOT_CATEGORY);
@@ -107,12 +190,12 @@ private function populate()
107190

108191
$repo
109192
->persistAsFirstChild($food)
110-
->persistAsFirstChildOf($fruits, $food)
111-
->persistAsFirstChildOf($vegetables, $food)
193+
->persistAsLastChildOf($fruits, $food)
194+
->persistAsLastChildOf($vegetables, $food)
112195
->persistAsLastChildOf($milk, $food)
113196
->persistAsLastChildOf($meat, $food)
114-
->persistAsFirstChildOf($oranges, $fruits)
115-
->persistAsFirstChildOf($citrons, $fruits);
197+
->persistAsLastChildOf($oranges, $fruits)
198+
->persistAsLastChildOf($citrons, $fruits);
116199

117200
$this->em->flush();
118201
}

0 commit comments

Comments
 (0)