source: pro-violet-viettel/sourcecode/application/libraries/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @ 345

Last change on this file since 345 was 345, checked in by quyenla, 11 years ago

collaborator page

File size: 23.0 KB
Line 
1<?php
2/*
3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 *
15 * This software consists of voluntary contributions made by many individuals
16 * and is licensed under the LGPL. For more information, see
17 * <http://www.doctrine-project.org>.
18 */
19
20namespace Doctrine\ORM\Internal\Hydration;
21
22use PDO,
23    Doctrine\ORM\Mapping\ClassMetadata,
24    Doctrine\ORM\PersistentCollection,
25    Doctrine\ORM\Query,
26    Doctrine\ORM\Event\LifecycleEventArgs,
27    Doctrine\ORM\Events,
28    Doctrine\Common\Collections\ArrayCollection,
29    Doctrine\Common\Collections\Collection,
30    Doctrine\ORM\Proxy\Proxy;
31
32/**
33 * The ObjectHydrator constructs an object graph out of an SQL result set.
34 *
35 * @since  2.0
36 * @author Roman Borschel <roman@code-factory.org>
37 * @author Guilherme Blanco <guilhermeblanoc@hotmail.com>
38 *
39 * @internal Highly performance-sensitive code.
40 */
41class ObjectHydrator extends AbstractHydrator
42{
43    /* Local ClassMetadata cache to avoid going to the EntityManager all the time.
44     * This local cache is maintained between hydration runs and not cleared.
45     */
46    private $_ce = array();
47
48    /* The following parts are reinitialized on every hydration run. */
49
50    private $_identifierMap;
51    private $_resultPointers;
52    private $_idTemplate;
53    private $_resultCounter;
54    private $_rootAliases = array();
55    private $_initializedCollections = array();
56    private $_existingCollections = array();
57
58
59    /** @override */
60    protected function prepare()
61    {
62        $this->_identifierMap =
63        $this->_resultPointers =
64        $this->_idTemplate = array();
65
66        $this->_resultCounter = 0;
67
68        if ( ! isset($this->_hints['deferEagerLoad'])) {
69            $this->_hints['deferEagerLoad'] = true;
70        }
71
72        foreach ($this->_rsm->aliasMap as $dqlAlias => $className) {
73            $this->_identifierMap[$dqlAlias] = array();
74            $this->_idTemplate[$dqlAlias]    = '';
75
76            if ( ! isset($this->_ce[$className])) {
77                $this->_ce[$className] = $this->_em->getClassMetadata($className);
78            }
79
80            // Remember which associations are "fetch joined", so that we know where to inject
81            // collection stubs or proxies and where not.
82            if ( ! isset($this->_rsm->relationMap[$dqlAlias])) {
83                continue;
84            }
85
86            if ( ! isset($this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]])) {
87                throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $this->_rsm->parentAliasMap[$dqlAlias]);
88            }
89
90            $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]];
91            $sourceClass     = $this->_getClassMetadata($sourceClassName);
92            $assoc           = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]];
93
94            $this->_hints['fetched'][$this->_rsm->parentAliasMap[$dqlAlias]][$assoc['fieldName']] = true;
95
96            if ($assoc['type'] === ClassMetadata::MANY_TO_MANY) {
97                continue;
98            }
99
100            // Mark any non-collection opposite sides as fetched, too.
101            if ($assoc['mappedBy']) {
102                $this->_hints['fetched'][$dqlAlias][$assoc['mappedBy']] = true;
103
104                continue;
105            }
106
107            // handle fetch-joined owning side bi-directional one-to-one associations
108            if ($assoc['inversedBy']) {
109                $class        = $this->_ce[$className];
110                $inverseAssoc = $class->associationMappings[$assoc['inversedBy']];
111
112                if ( ! ($inverseAssoc['type'] & ClassMetadata::TO_ONE)) {
113                    continue;
114                }
115
116                $this->_hints['fetched'][$dqlAlias][$inverseAssoc['fieldName']] = true;
117            }
118        }
119    }
120
121    /**
122     * {@inheritdoc}
123     */
124    protected function cleanup()
125    {
126        $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true;
127
128        parent::cleanup();
129
130        $this->_identifierMap =
131        $this->_initializedCollections =
132        $this->_existingCollections =
133        $this->_resultPointers = array();
134
135        if ($eagerLoad) {
136            $this->_em->getUnitOfWork()->triggerEagerLoads();
137        }
138    }
139
140    /**
141     * {@inheritdoc}
142     */
143    protected function hydrateAllData()
144    {
145        $result = array();
146        $cache  = array();
147
148        while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) {
149            $this->hydrateRowData($row, $cache, $result);
150        }
151
152        // Take snapshots from all newly initialized collections
153        foreach ($this->_initializedCollections as $coll) {
154            $coll->takeSnapshot();
155        }
156
157        return $result;
158    }
159
160    /**
161     * Initializes a related collection.
162     *
163     * @param object $entity The entity to which the collection belongs.
164     * @param ClassMetadata $class
165     * @param string $name The name of the field on the entity that holds the collection.
166     * @param string $parentDqlAlias Alias of the parent fetch joining this collection.
167     */
168    private function _initRelatedCollection($entity, $class, $fieldName, $parentDqlAlias)
169    {
170        $oid      = spl_object_hash($entity);
171        $relation = $class->associationMappings[$fieldName];
172        $value    = $class->reflFields[$fieldName]->getValue($entity);
173
174        if ($value === null) {
175            $value = new ArrayCollection;
176        }
177
178        if ( ! $value instanceof PersistentCollection) {
179            $value = new PersistentCollection(
180                $this->_em, $this->_ce[$relation['targetEntity']], $value
181            );
182            $value->setOwner($entity, $relation);
183
184            $class->reflFields[$fieldName]->setValue($entity, $value);
185            $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value);
186
187            $this->_initializedCollections[$oid . $fieldName] = $value;
188        } else if (
189            isset($this->_hints[Query::HINT_REFRESH]) ||
190            isset($this->_hints['fetched'][$parentDqlAlias][$fieldName]) &&
191             ! $value->isInitialized()
192        ) {
193            // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED!
194            $value->setDirty(false);
195            $value->setInitialized(true);
196            $value->unwrap()->clear();
197
198            $this->_initializedCollections[$oid . $fieldName] = $value;
199        } else {
200            // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN!
201            $this->_existingCollections[$oid . $fieldName] = $value;
202        }
203
204        return $value;
205    }
206
207    /**
208     * Gets an entity instance.
209     *
210     * @param $data The instance data.
211     * @param $dqlAlias The DQL alias of the entity's class.
212     * @return object The entity.
213     */
214    private function _getEntity(array $data, $dqlAlias)
215    {
216        $className = $this->_rsm->aliasMap[$dqlAlias];
217
218        if (isset($this->_rsm->discriminatorColumns[$dqlAlias])) {
219            $discrColumn = $this->_rsm->metaMappings[$this->_rsm->discriminatorColumns[$dqlAlias]];
220
221            if ($data[$discrColumn] === "") {
222                throw HydrationException::emptyDiscriminatorValue($dqlAlias);
223            }
224
225            $className = $this->_ce[$className]->discriminatorMap[$data[$discrColumn]];
226
227            unset($data[$discrColumn]);
228        }
229
230        if (isset($this->_hints[Query::HINT_REFRESH_ENTITY]) && isset($this->_rootAliases[$dqlAlias])) {
231            $this->registerManaged($this->_ce[$className], $this->_hints[Query::HINT_REFRESH_ENTITY], $data);
232        }
233
234        $this->_hints['fetchAlias'] = $dqlAlias;
235
236        return $this->_uow->createEntity($className, $data, $this->_hints);
237    }
238
239    private function _getEntityFromIdentityMap($className, array $data)
240    {
241        // TODO: Abstract this code and UnitOfWork::createEntity() equivalent?
242        $class = $this->_ce[$className];
243
244        /* @var $class ClassMetadata */
245        if ($class->isIdentifierComposite) {
246            $idHash = '';
247            foreach ($class->identifier as $fieldName) {
248                if (isset($class->associationMappings[$fieldName])) {
249                    $idHash .= $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']] . ' ';
250                } else {
251                    $idHash .= $data[$fieldName] . ' ';
252                }
253            }
254            return $this->_uow->tryGetByIdHash(rtrim($idHash), $class->rootEntityName);
255        } else if (isset($class->associationMappings[$class->identifier[0]])) {
256            return $this->_uow->tryGetByIdHash($data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']], $class->rootEntityName);
257        } else {
258            return $this->_uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName);
259        }
260    }
261
262    /**
263     * Gets a ClassMetadata instance from the local cache.
264     * If the instance is not yet in the local cache, it is loaded into the
265     * local cache.
266     *
267     * @param string $className The name of the class.
268     * @return ClassMetadata
269     */
270    private function _getClassMetadata($className)
271    {
272        if ( ! isset($this->_ce[$className])) {
273            $this->_ce[$className] = $this->_em->getClassMetadata($className);
274        }
275
276        return $this->_ce[$className];
277    }
278
279    /**
280     * Hydrates a single row in an SQL result set.
281     *
282     * @internal
283     * First, the data of the row is split into chunks where each chunk contains data
284     * that belongs to a particular component/class. Afterwards, all these chunks
285     * are processed, one after the other. For each chunk of class data only one of the
286     * following code paths is executed:
287     *
288     * Path A: The data chunk belongs to a joined/associated object and the association
289     *         is collection-valued.
290     * Path B: The data chunk belongs to a joined/associated object and the association
291     *         is single-valued.
292     * Path C: The data chunk belongs to a root result element/object that appears in the topmost
293     *         level of the hydrated result. A typical example are the objects of the type
294     *         specified by the FROM clause in a DQL query.
295     *
296     * @param array $data The data of the row to process.
297     * @param array $cache The cache to use.
298     * @param array $result The result array to fill.
299     */
300    protected function hydrateRowData(array $row, array &$cache, array &$result)
301    {
302        // Initialize
303        $id = $this->_idTemplate; // initialize the id-memory
304        $nonemptyComponents = array();
305        // Split the row data into chunks of class data.
306        $rowData = $this->gatherRowData($row, $cache, $id, $nonemptyComponents);
307
308        // Extract scalar values. They're appended at the end.
309        if (isset($rowData['scalars'])) {
310            $scalars = $rowData['scalars'];
311
312            unset($rowData['scalars']);
313
314            if (empty($rowData)) {
315                ++$this->_resultCounter;
316            }
317        }
318
319        // Hydrate the data chunks
320        foreach ($rowData as $dqlAlias => $data) {
321            $entityName = $this->_rsm->aliasMap[$dqlAlias];
322
323            if (isset($this->_rsm->parentAliasMap[$dqlAlias])) {
324                // It's a joined result
325
326                $parentAlias = $this->_rsm->parentAliasMap[$dqlAlias];
327                // we need the $path to save into the identifier map which entities were already
328                // seen for this parent-child relationship
329                $path = $parentAlias . '.' . $dqlAlias;
330
331                // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs
332                if (!isset($nonemptyComponents[$parentAlias])) {
333                    // TODO: Add special case code where we hydrate the right join objects into identity map at least
334                    continue;
335                }
336
337                // Get a reference to the parent object to which the joined element belongs.
338                if ($this->_rsm->isMixed && isset($this->_rootAliases[$parentAlias])) {
339                    $first = reset($this->_resultPointers);
340                    $parentObject = $first[key($first)];
341                } else if (isset($this->_resultPointers[$parentAlias])) {
342                    $parentObject = $this->_resultPointers[$parentAlias];
343                } else {
344                    // Parent object of relation not found, so skip it.
345                    continue;
346                }
347
348                $parentClass = $this->_ce[$this->_rsm->aliasMap[$parentAlias]];
349                $oid = spl_object_hash($parentObject);
350                $relationField = $this->_rsm->relationMap[$dqlAlias];
351                $relation = $parentClass->associationMappings[$relationField];
352                $reflField = $parentClass->reflFields[$relationField];
353
354                // Check the type of the relation (many or single-valued)
355                if ( ! ($relation['type'] & ClassMetadata::TO_ONE)) {
356                    $reflFieldValue = $reflField->getValue($parentObject);
357                    // PATH A: Collection-valued association
358                    if (isset($nonemptyComponents[$dqlAlias])) {
359                        $collKey = $oid . $relationField;
360                        if (isset($this->_initializedCollections[$collKey])) {
361                            $reflFieldValue = $this->_initializedCollections[$collKey];
362                        } else if ( ! isset($this->_existingCollections[$collKey])) {
363                            $reflFieldValue = $this->_initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
364                        }
365
366                        $indexExists = isset($this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]);
367                        $index = $indexExists ? $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false;
368                        $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false;
369
370                        if ( ! $indexExists || ! $indexIsValid) {
371                            if (isset($this->_existingCollections[$collKey])) {
372                                // Collection exists, only look for the element in the identity map.
373                                if ($element = $this->_getEntityFromIdentityMap($entityName, $data)) {
374                                    $this->_resultPointers[$dqlAlias] = $element;
375                                } else {
376                                    unset($this->_resultPointers[$dqlAlias]);
377                                }
378                            } else {
379                                $element = $this->_getEntity($data, $dqlAlias);
380
381                                if (isset($this->_rsm->indexByMap[$dqlAlias])) {
382                                    $indexValue = $row[$this->_rsm->indexByMap[$dqlAlias]];
383                                    $reflFieldValue->hydrateSet($indexValue, $element);
384                                    $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue;
385                                } else {
386                                    $reflFieldValue->hydrateAdd($element);
387                                    $reflFieldValue->last();
388                                    $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key();
389                                }
390                                // Update result pointer
391                                $this->_resultPointers[$dqlAlias] = $element;
392                            }
393                        } else {
394                            // Update result pointer
395                            $this->_resultPointers[$dqlAlias] = $reflFieldValue[$index];
396                        }
397                    } else if ( ! $reflFieldValue) {
398                        $reflFieldValue = $this->_initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
399                    } else if ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false) {
400                        $reflFieldValue->setInitialized(true);
401                    }
402
403                } else {
404                    // PATH B: Single-valued association
405                    $reflFieldValue = $reflField->getValue($parentObject);
406                    if ( ! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH]) || ($reflFieldValue instanceof Proxy && !$reflFieldValue->__isInitialized__)) {
407                        // we only need to take action if this value is null,
408                        // we refresh the entity or its an unitialized proxy.
409                        if (isset($nonemptyComponents[$dqlAlias])) {
410                            $element = $this->_getEntity($data, $dqlAlias);
411                            $reflField->setValue($parentObject, $element);
412                            $this->_uow->setOriginalEntityProperty($oid, $relationField, $element);
413                            $targetClass = $this->_ce[$relation['targetEntity']];
414
415                            if ($relation['isOwningSide']) {
416                                //TODO: Just check hints['fetched'] here?
417                                // If there is an inverse mapping on the target class its bidirectional
418                                if ($relation['inversedBy']) {
419                                    $inverseAssoc = $targetClass->associationMappings[$relation['inversedBy']];
420                                    if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) {
421                                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject);
422                                        $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $inverseAssoc['fieldName'], $parentObject);
423                                    }
424                                } else if ($parentClass === $targetClass && $relation['mappedBy']) {
425                                    // Special case: bi-directional self-referencing one-one on the same class
426                                    $targetClass->reflFields[$relationField]->setValue($element, $parentObject);
427                                }
428                            } else {
429                                // For sure bidirectional, as there is no inverse side in unidirectional mappings
430                                $targetClass->reflFields[$relation['mappedBy']]->setValue($element, $parentObject);
431                                $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $relation['mappedBy'], $parentObject);
432                            }
433                            // Update result pointer
434                            $this->_resultPointers[$dqlAlias] = $element;
435                        } else {
436                            $this->_uow->setOriginalEntityProperty($oid, $relationField, null);
437                        }
438                        // else leave $reflFieldValue null for single-valued associations
439                    } else {
440                        // Update result pointer
441                        $this->_resultPointers[$dqlAlias] = $reflFieldValue;
442                    }
443                }
444            } else {
445                // PATH C: Its a root result element
446                $this->_rootAliases[$dqlAlias] = true; // Mark as root alias
447                $entityKey = $this->_rsm->entityMappings[$dqlAlias] ?: 0;
448
449                // if this row has a NULL value for the root result id then make it a null result.
450                if ( ! isset($nonemptyComponents[$dqlAlias]) ) {
451                    if ($this->_rsm->isMixed) {
452                        $result[] = array($entityKey => null);
453                    } else {
454                        $result[] = null;
455                    }
456                    $resultKey = $this->_resultCounter;
457                    ++$this->_resultCounter;
458                    continue;
459                }
460
461                // check for existing result from the iterations before
462                if ( ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) {
463                    $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias);
464                    if ($this->_rsm->isMixed) {
465                        $element = array($entityKey => $element);
466                    }
467
468                    if (isset($this->_rsm->indexByMap[$dqlAlias])) {
469                        $resultKey = $row[$this->_rsm->indexByMap[$dqlAlias]];
470
471                        if (isset($this->_hints['collection'])) {
472                            $this->_hints['collection']->hydrateSet($resultKey, $element);
473                        }
474
475                        $result[$resultKey] = $element;
476                    } else {
477                        $resultKey = $this->_resultCounter;
478                        ++$this->_resultCounter;
479
480                        if (isset($this->_hints['collection'])) {
481                            $this->_hints['collection']->hydrateAdd($element);
482                        }
483
484                        $result[] = $element;
485                    }
486
487                    $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey;
488
489                    // Update result pointer
490                    $this->_resultPointers[$dqlAlias] = $element;
491
492                } else {
493                    // Update result pointer
494                    $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]];
495                    $this->_resultPointers[$dqlAlias] = $result[$index];
496                    $resultKey = $index;
497                    /*if ($this->_rsm->isMixed) {
498                        $result[] = $result[$index];
499                        ++$this->_resultCounter;
500                    }*/
501                }
502            }
503        }
504
505        // Append scalar values to mixed result sets
506        if (isset($scalars)) {
507            if ( ! isset($resultKey) ) {
508                if (isset($this->_rsm->indexByMap['scalars'])) {
509                    $resultKey = $row[$this->_rsm->indexByMap['scalars']];
510                } else {
511                    $resultKey = $this->_resultCounter - 1;
512                }
513            }
514
515            foreach ($scalars as $name => $value) {
516                $result[$resultKey][$name] = $value;
517            }
518        }
519    }
520
521    /**
522     * When executed in a hydrate() loop we may have to clear internal state to
523     * decrease memory consumption.
524     */
525    public function onClear($eventArgs)
526    {
527        parent::onClear($eventArgs);
528
529        $aliases              = array_keys($this->_identifierMap);
530        $this->_identifierMap = array();
531
532        foreach ($aliases as $alias) {
533            $this->_identifierMap[$alias] = array();
534        }
535    }
536}
Note: See TracBrowser for help on using the repository browser.