Skip to content

Commit 1a5f953

Browse files
authored
Merge pull request #1780 from iluuu1994/v2.4.x
Added `TreeObjectHydrator` class
2 parents 58dfe52 + b6e1f57 commit 1a5f953

File tree

4 files changed

+514
-0
lines changed

4 files changed

+514
-0
lines changed

doc/tree.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Thanks for contributions to:
2222
- **[everzet](http://github.com/everzet) Kudryashov Konstantin** for TreeLevel implementation
2323
- **[stof](http://github.com/stof) Christophe Coevoet** for getTreeLeafs function
2424

25+
Update **2017-04-22**
26+
27+
- Added the `TreeObjectHydrator` class for building trees from entities
28+
2529
Update **2012-06-28**
2630

2731
- Added "buildTree" functionality support for Closure and Materialized Path strategies
@@ -636,6 +640,31 @@ $controller = $this;
636640

637641
<a name="advanced-examples"></a>
638642

643+
## Building trees from your entities
644+
645+
You can use the `childrenHierarchy` method to build an array tree from your result set.
646+
However, sometimes it is more convenient to work with the entities directly. The `TreeObjectHydrator`
647+
lets you build a tree from your entities instead, without triggering any more queries.
648+
649+
First, you have to register the hydrator in your Doctrine entity manager.
650+
651+
```php
652+
<?php
653+
$em->getConfiguration()->addCustomHydrationMode('tree', 'Gedmo\Tree\Hydrator\ORM\TreeObjectHydrator');
654+
```
655+
656+
The hydrator requires the `HINT_INCLUDE_META_COLUMNS` query hint. Without it the hydrator will not work!
657+
Other than that, the usage is straight-forward.
658+
659+
```php
660+
<?php
661+
$repo = $em->getRepository('Entity\Category');
662+
663+
$tree = $repo->createQueryBuilder('node')->getQuery()
664+
->setHint(\Doctrine\ORM\Query::HINT_INCLUDE_META_COLUMNS, true)
665+
->getResult('tree');
666+
```
667+
639668
## Advanced examples:
640669

641670
### Nesting Translatable and Sluggable extensions
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<?php
2+
3+
namespace Gedmo\Tree\Hydrator\ORM;
4+
5+
use Doctrine\Common\Collections\AbstractLazyCollection;
6+
use Doctrine\Common\Collections\ArrayCollection;
7+
use Doctrine\ORM\EntityManagerInterface;
8+
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
9+
use Doctrine\ORM\Proxy\Proxy;
10+
use Gedmo\Tool\Wrapper\EntityWrapper;
11+
use Gedmo\Tree\TreeListener;
12+
13+
/**
14+
* Automatically maps the parent and children properties of Tree nodes
15+
*
16+
* @author Ilija Tovilo <[email protected]>
17+
* @link http://www.gediminasm.org
18+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
19+
*/
20+
class TreeObjectHydrator extends ObjectHydrator
21+
{
22+
/**
23+
* @var array
24+
*/
25+
private $config;
26+
27+
/**
28+
* @var string
29+
*/
30+
private $idField;
31+
32+
/**
33+
* @var string
34+
*/
35+
private $parentField;
36+
37+
/**
38+
* @var string
39+
*/
40+
private $childrenField;
41+
42+
/**
43+
* We hook into the `hydrateAllData` to map the children collection of the entity
44+
*
45+
* {@inheritdoc}
46+
*/
47+
protected function hydrateAllData()
48+
{
49+
$data = parent::hydrateAllData();
50+
51+
if (count($data) === 0) {
52+
return $data;
53+
}
54+
55+
$listener = $this->getTreeListener($this->_em);
56+
$entityClass = $this->getEntityClassFromHydratedData($data);
57+
$this->config = $listener->getConfiguration($this->_em, $entityClass);
58+
$this->idField = $this->getIdField($entityClass);
59+
$this->parentField = $this->getParentField();
60+
$this->childrenField = $this->getChildrenField($entityClass);
61+
62+
63+
$childrenHashmap = $this->buildChildrenHashmap($data);
64+
$this->populateChildrenArray($data, $childrenHashmap);
65+
66+
// Only return root elements or elements who's parents haven't been fetched
67+
// The sub-nodes will be accessible via the `children` property
68+
return $this->getRootNodes($data);
69+
}
70+
71+
/**
72+
* Creates a hashmap to quickly find the children of a node
73+
*
74+
* ```
75+
* [parentId => [child1, child2, ...], ...]
76+
* ```
77+
*
78+
* @param array $nodes
79+
* @return array
80+
*/
81+
protected function buildChildrenHashmap($nodes)
82+
{
83+
$r = array();
84+
85+
foreach ($nodes as $node) {
86+
$parentProxy = $this->getPropertyValue($node, $this->config['parent']);
87+
$parentId = null;
88+
89+
if ($parentProxy !== null) {
90+
$parentId = $this->getPropertyValue($parentProxy, $this->idField);
91+
}
92+
93+
$r[$parentId][] = $node;
94+
}
95+
96+
return $r;
97+
}
98+
99+
/**
100+
* @param array $nodes
101+
* @param array $childrenHashmap
102+
*/
103+
protected function populateChildrenArray($nodes, $childrenHashmap)
104+
{
105+
foreach ($nodes as $node) {
106+
$nodeId = $this->getPropertyValue($node, $this->idField);
107+
$childrenCollection = $this->getPropertyValue($node, $this->childrenField);
108+
109+
if ($childrenCollection === null) {
110+
$childrenCollection = new ArrayCollection();
111+
$this->setPropertyValue($node, $this->childrenField, $childrenCollection);
112+
}
113+
114+
// Mark all children collections as initialized to avoid select queries
115+
if ($childrenCollection instanceof AbstractLazyCollection) {
116+
$childrenCollection->setInitialized(true);
117+
}
118+
119+
if (!isset($childrenHashmap[$nodeId])) {
120+
continue;
121+
}
122+
123+
$childrenCollection->clear();
124+
125+
foreach ($childrenHashmap[$nodeId] as $child) {
126+
$childrenCollection->add($child);
127+
}
128+
}
129+
}
130+
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+
178+
/**
179+
* @return string
180+
*/
181+
protected function getIdField($entityClass)
182+
{
183+
$meta = $this->getClassMetadata($entityClass);
184+
return $meta->getSingleIdentifierFieldName();
185+
}
186+
187+
/**
188+
* @return string
189+
*/
190+
protected function getParentField()
191+
{
192+
if (!isset($this->config['parent'])) {
193+
throw new \Gedmo\Exception\InvalidMappingException('The `parent` property is required for the TreeHydrator to work');
194+
}
195+
196+
return $this->config['parent'];
197+
}
198+
199+
/**
200+
* @return string
201+
*/
202+
protected function getChildrenField($entityClass)
203+
{
204+
$meta = $this->getClassMetadata($entityClass);
205+
206+
foreach ($meta->getReflectionProperties() as $property) {
207+
208+
// Skip properties that have no association
209+
if (!$meta->hasAssociation($property->getName())) {
210+
continue;
211+
}
212+
213+
$associationMapping = $meta->getAssociationMapping($property->getName());
214+
215+
// Make sure the association is mapped by the parent property
216+
if ($associationMapping['mappedBy'] !== $this->parentField) {
217+
continue;
218+
}
219+
220+
return $associationMapping['fieldName'];
221+
}
222+
223+
throw new \Gedmo\Exception\InvalidMappingException('The children property could not found. It is identified through the `mappedBy` annotation to your parent property.');
224+
}
225+
226+
/**
227+
* @param EntityManagerInterface $em
228+
* @return TreeListener
229+
*/
230+
protected function getTreeListener(EntityManagerInterface $em)
231+
{
232+
foreach ($em->getEventManager()->getListeners() as $listeners) {
233+
foreach ($listeners as $listener) {
234+
if ($listener instanceof TreeListener) {
235+
return $listener;
236+
}
237+
}
238+
}
239+
240+
throw new \Gedmo\Exception\InvalidMappingException('Tree listener was not found on your entity manager, it must be hooked into the event manager');
241+
}
242+
243+
/**
244+
* @param array $data
245+
* @return string
246+
*/
247+
protected function getEntityClassFromHydratedData($data)
248+
{
249+
$firstMappedEntity = array_values($data);
250+
$firstMappedEntity = $firstMappedEntity[0];
251+
return get_class($firstMappedEntity);
252+
}
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+
}
265+
}

tests/Gedmo/Tree/Fixture/RootCategory.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,14 @@ public function getLevel()
105105
{
106106
return $this->level;
107107
}
108+
109+
public function getChildren()
110+
{
111+
return $this->children;
112+
}
113+
114+
public function setChildren($children)
115+
{
116+
$this->children = $children;
117+
}
108118
}

0 commit comments

Comments
 (0)