Failed Conditions
Pull Request — master (#6743)
by Grégoire
18:17 queued 12:33
created

ObjectHydrator::hydrateRowData()   F

Complexity

Conditions 50
Paths > 20000

Size

Total Lines 262
Code Lines 148

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 136
CRAP Score 50

Importance

Changes 0
Metric Value
cc 50
eloc 148
nc 31096
nop 2
dl 0
loc 262
ccs 136
cts 136
cp 1
crap 50
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Internal\Hydration;
6
7
use Doctrine\Common\Collections\Collection;
8
use Doctrine\ORM\Mapping\ClassMetadata;
9
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
10
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
11
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
12
use Doctrine\ORM\PersistentCollection;
13
use Doctrine\ORM\Query;
14
use Doctrine\ORM\UnitOfWork;
15
use PDO;
16
use ProxyManager\Proxy\GhostObjectInterface;
17
18
/**
19
 * The ObjectHydrator constructs an object graph out of an SQL result set.
20
 *
21
 * Internal note: Highly performance-sensitive code.
22
 */
23
class ObjectHydrator extends AbstractHydrator
24
{
25
    /**
26
     * @var mixed[][]
27
     */
28
    private $identifierMap = [];
29
30
    /**
31
     * @var mixed[]
32
     */
33
    private $resultPointers = [];
34
35
    /**
36
     * @var string[]
37
     */
38
    private $idTemplate = [];
39
40
    /**
41
     * @var int
42
     */
43
    private $resultCounter = 0;
44
45
    /**
46
     * @var bool
47
     */
48
    private $rootAliases = [];
49
50
    /**
51
     * @var Collection[]|object[][]
52
     */
53
    private $initializedCollections = [];
54
55
    /**
56
     * @var Collection[]|object[][]
57
     */
58
    private $existingCollections = [];
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 617
    protected function prepare()
64
    {
65 617
        if (! isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD])) {
66 523
            $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] = true;
67
        }
68
69 617
        foreach ($this->rsm->aliasMap as $dqlAlias => $className) {
70 585
            $this->identifierMap[$dqlAlias] = [];
71 585
            $this->idTemplate[$dqlAlias]    = '';
72
73
            // Remember which associations are "fetch joined", so that we know where to inject
74
            // collection stubs or proxies and where not.
75 585
            if (! isset($this->rsm->relationMap[$dqlAlias])) {
76 585
                continue;
77
            }
78
79 287
            $parent = $this->rsm->parentAliasMap[$dqlAlias];
80
81 287
            if (! isset($this->rsm->aliasMap[$parent])) {
82
                throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent);
83
            }
84
85 287
            $sourceClassName = $this->rsm->aliasMap[$parent];
86 287
            $sourceClass     = $this->getClassMetadata($sourceClassName);
87 287
            $association     = $sourceClass->getProperty($this->rsm->relationMap[$dqlAlias]);
88
89 287
            $this->hints['fetched'][$parent][$association->getName()] = true;
90
91 287
            if ($association instanceof ManyToManyAssociationMetadata) {
92 35
                continue;
93
            }
94
95
            // Mark any non-collection opposite sides as fetched, too.
96 266
            if ($association->getMappedBy()) {
97 225
                $this->hints['fetched'][$dqlAlias][$association->getMappedBy()] = true;
98
99 225
                continue;
100
            }
101
102
            // handle fetch-joined owning side bi-directional one-to-one associations
103 54
            if ($association->getInversedBy()) {
104 36
                $class        = $this->getClassMetadata($className);
105 36
                $inverseAssoc = $class->getProperty($association->getInversedBy());
106
107 36
                if (! ($inverseAssoc instanceof ToOneAssociationMetadata)) {
108 22
                    continue;
109
                }
110
111 33
                $this->hints['fetched'][$dqlAlias][$inverseAssoc->getName()] = true;
112
            }
113
        }
114 617
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 615
    protected function cleanup()
120
    {
121 615
        $eagerLoad = (isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD])) && $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] === true;
122
123 615
        parent::cleanup();
124
125 615
        $this->identifierMap          =
126 615
        $this->initializedCollections =
127 615
        $this->existingCollections    =
128 615
        $this->resultPointers         = [];
129
130 615
        if ($eagerLoad) {
131 615
            $this->uow->triggerEagerLoads();
132
        }
133
134 615
        $this->uow->hydrationComplete();
135 615
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140 611
    protected function hydrateAllData()
141
    {
142 611
        $result = [];
143
144 611
        while ($row = $this->stmt->fetch(PDO::FETCH_ASSOC)) {
145 573
            $this->hydrateRowData($row, $result);
146
        }
147
148
        // Take snapshots from all newly initialized collections
149 611
        foreach ($this->initializedCollections as $coll) {
150 66
            $coll->takeSnapshot();
151
        }
152
153 611
        return $result;
154
    }
155
156
    /**
157
     * Initializes a related collection.
158
     *
159
     * @param object        $entity         The entity to which the collection belongs.
160
     * @param ClassMetadata $class
161
     * @param string        $fieldName      The name of the field on the entity that holds the collection.
162
     * @param string        $parentDqlAlias Alias of the parent fetch joining this collection.
163
     *
164
     * @return \Doctrine\ORM\PersistentCollection
165
     */
166 98
    private function initRelatedCollection($entity, $class, $fieldName, $parentDqlAlias)
167
    {
168
        /** @var ToManyAssociationMetadata $association */
169 98
        $association = $class->getProperty($fieldName);
170 98
        $value       = $association->getValue($entity);
171 98
        $oid         = spl_object_id($entity);
172
173 98
        if (! $value instanceof PersistentCollection) {
174 62
            $value = $association->wrap($entity, $value, $this->em);
175
176 62
            $association->setValue($entity, $value);
177
178 62
            $this->uow->setOriginalEntityProperty($oid, $fieldName, $value);
179
180 62
            $this->initializedCollections[$oid . $fieldName] = $value;
181 39
        } elseif (isset($this->hints[Query::HINT_REFRESH]) ||
182 39
            (isset($this->hints['fetched'][$parentDqlAlias][$fieldName]) && ! $value->isInitialized())
183
        ) {
184
            // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED!
185 6
            $value->setDirty(false);
186 6
            $value->setInitialized(true);
187 6
            $value->unwrap()->clear();
188
189 6
            $this->initializedCollections[$oid . $fieldName] = $value;
190
        } else {
191
            // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN!
192 34
            $this->existingCollections[$oid . $fieldName] = $value;
193
        }
194
195 98
        return $value;
196
    }
197
198
    /**
199
     * Gets an entity instance.
200
     *
201
     * @param mixed[] $data     The instance data.
202
     * @param string  $dqlAlias The DQL alias of the entity's class.
203
     *
204
     * @return object The entity.
205
     *
206
     * @throws HydrationException
207
     */
208 545
    private function getEntity(array $data, $dqlAlias)
209
    {
210 545
        $className = $this->rsm->aliasMap[$dqlAlias];
211
212 545
        if (isset($this->rsm->discriminatorColumns[$dqlAlias])) {
213 63
            $fieldName = $this->rsm->discriminatorColumns[$dqlAlias];
214
215 63
            if (! isset($this->rsm->metaMappings[$fieldName])) {
216
                throw HydrationException::missingDiscriminatorMetaMappingColumn($className, $fieldName, $dqlAlias);
217
            }
218
219 63
            $discrColumn = $this->rsm->metaMappings[$fieldName];
220
221 63
            if (! isset($data[$discrColumn])) {
222
                throw HydrationException::missingDiscriminatorColumn($className, $discrColumn, $dqlAlias);
223
            }
224
225 63
            if ($data[$discrColumn] === '') {
226
                throw HydrationException::emptyDiscriminatorValue($dqlAlias);
227
            }
228
229 63
            $discrMap           = $this->metadataCache[$className]->discriminatorMap;
230 63
            $discriminatorValue = (string) $data[$discrColumn];
231
232 63
            if (! isset($discrMap[$discriminatorValue])) {
233
                throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap));
234
            }
235
236 63
            $className = $discrMap[$discriminatorValue];
237
238 63
            unset($data[$discrColumn]);
239
        }
240
241 545
        if (isset($this->hints[Query::HINT_REFRESH_ENTITY], $this->rootAliases[$dqlAlias])) {
242 20
            $id = $this->em->getIdentifierFlattener()->flattenIdentifier($this->metadataCache[$className], $data);
243
244 20
            $this->em->getUnitOfWork()->registerManaged($this->hints[Query::HINT_REFRESH_ENTITY], $id, $data);
245
        }
246
247 545
        $this->hints['fetchAlias'] = $dqlAlias;
248
249 545
        return $this->uow->createEntity($className, $data, $this->hints);
250
    }
251
252
    /**
253
     * @param string  $className
254
     * @param mixed[] $data
255
     *
256
     * @return mixed
257
     */
258 34
    private function getEntityFromIdentityMap($className, array $data)
259
    {
260
        /* @var ClassMetadata $class */
261 34
        $class = $this->metadataCache[$className];
262 34
        $id    = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $data);
263
264 34
        return $this->uow->tryGetById($id, $class->getRootClassName());
265
    }
266
267
    /**
268
     * Hydrates a single row in an SQL result set.
269
     *
270
     * @internal
271
     * First, the data of the row is split into chunks where each chunk contains data
272
     * that belongs to a particular component/class. Afterwards, all these chunks
273
     * are processed, one after the other. For each chunk of class data only one of the
274
     * following code paths is executed:
275
     *
276
     * Path A: The data chunk belongs to a joined/associated object and the association
277
     *         is collection-valued.
278
     * Path B: The data chunk belongs to a joined/associated object and the association
279
     *         is single-valued.
280
     * Path C: The data chunk belongs to a root result element/object that appears in the topmost
281
     *         level of the hydrated result. A typical example are the objects of the type
282
     *         specified by the FROM clause in a DQL query.
283
     *
284
     * @param mixed[] $row    The data of the row to process.
285
     * @param mixed[] $result The result array to fill.
286
     */
287 578
    protected function hydrateRowData(array $row, array &$result)
288
    {
289
        // Initialize
290 578
        $id                 = $this->idTemplate; // initialize the id-memory
291 578
        $nonemptyComponents = [];
292
        // Split the row data into chunks of class data.
293 578
        $rowData = $this->gatherRowData($row, $id, $nonemptyComponents);
294
295
        // reset result pointers for each data row
296 578
        $this->resultPointers = [];
297
298
        // Hydrate the data chunks
299 578
        foreach ($rowData['data'] as $dqlAlias => $data) {
300 545
            $entityName = $this->rsm->aliasMap[$dqlAlias];
301
302 545
            if (isset($this->rsm->parentAliasMap[$dqlAlias])) {
303
                // It's a joined result
304
305 282
                $parentAlias = $this->rsm->parentAliasMap[$dqlAlias];
306
                // we need the $path to save into the identifier map which entities were already
307
                // seen for this parent-child relationship
308 282
                $path = $parentAlias . '.' . $dqlAlias;
309
310
                // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs
311 282
                if (! isset($nonemptyComponents[$parentAlias])) {
312
                    // TODO: Add special case code where we hydrate the right join objects into identity map at least
313 2
                    continue;
314
                }
315
316 282
                $parentClass   = $this->metadataCache[$this->rsm->aliasMap[$parentAlias]];
317 282
                $relationField = $this->rsm->relationMap[$dqlAlias];
318 282
                $association   = $parentClass->getProperty($relationField);
319
320
                // Get a reference to the parent object to which the joined element belongs.
321 282
                if ($this->rsm->isMixed && isset($this->rootAliases[$parentAlias])) {
322 18
                    $objectClass  = $this->resultPointers[$parentAlias];
323 18
                    $parentObject = $objectClass[key($objectClass)];
324 266
                } elseif (isset($this->resultPointers[$parentAlias])) {
325 266
                    $parentObject = $this->resultPointers[$parentAlias];
326
                } else {
327
                    // Parent object of relation not found, mark as not-fetched again
328 2
                    $element = $this->getEntity($data, $dqlAlias);
329
330
                    // Update result pointer and provide initial fetch data for parent
331 2
                    $this->resultPointers[$dqlAlias]               = $element;
332 2
                    $rowData['data'][$parentAlias][$relationField] = $element;
333
334
                    // Mark as not-fetched again
335 2
                    unset($this->hints['fetched'][$parentAlias][$relationField]);
336 2
                    continue;
337
                }
338
339 282
                $oid = spl_object_id($parentObject);
340
341
                // Check the type of the relation (many or single-valued)
342 282
                if (! ($association instanceof ToOneAssociationMetadata)) {
343
                    // PATH A: Collection-valued association
344 99
                    $reflFieldValue = $association->getValue($parentObject);
345
346 99
                    if (isset($nonemptyComponents[$dqlAlias])) {
347 95
                        $collKey = $oid . $relationField;
348 95
                        if (isset($this->initializedCollections[$collKey])) {
349 42
                            $reflFieldValue = $this->initializedCollections[$collKey];
350 95
                        } elseif (! isset($this->existingCollections[$collKey])) {
351 95
                            $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
352
                        }
353
354 95
                        $indexExists  = isset($this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]);
355 95
                        $index        = $indexExists ? $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false;
356 95
                        $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false;
357
358 95
                        if (! $indexExists || ! $indexIsValid) {
359 95
                            if (isset($this->existingCollections[$collKey])) {
360 34
                                $element = $this->getEntityFromIdentityMap($entityName, $data);
361
362
                                // Collection exists, only look for the element in the identity map.
363 34
                                if ($element) {
364 34
                                    $this->resultPointers[$dqlAlias] = $element;
365
                                } else {
366 34
                                    unset($this->resultPointers[$dqlAlias]);
367
                                }
368
                            } else {
369 63
                                $element = $this->getEntity($data, $dqlAlias);
370
371 63
                                if (isset($this->rsm->indexByMap[$dqlAlias])) {
372 7
                                    $indexValue = $row[$this->rsm->indexByMap[$dqlAlias]];
373 7
                                    $reflFieldValue->hydrateSet($indexValue, $element);
374 7
                                    $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue;
375
                                } else {
376 56
                                    $reflFieldValue->hydrateAdd($element);
377 56
                                    $reflFieldValue->last();
378 56
                                    $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key();
379
                                }
380
                                // Update result pointer
381 95
                                $this->resultPointers[$dqlAlias] = $element;
382
                            }
383
                        } else {
384
                            // Update result pointer
385 95
                            $this->resultPointers[$dqlAlias] = $reflFieldValue[$index];
386
                        }
387 9
                    } elseif (! $reflFieldValue) {
388 5
                        $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
389 6
                    } elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false) {
390 99
                        $reflFieldValue->setInitialized(true);
391
                    }
392
                } else {
393
                    // PATH B: Single-valued association
394 204
                    $reflFieldValue = $association->getValue($parentObject);
395
396 204
                    if (! $reflFieldValue || isset($this->hints[Query::HINT_REFRESH]) ||
397 204
                        ($reflFieldValue instanceof GhostObjectInterface && ! $reflFieldValue->isProxyInitialized())) {
398
                        // we only need to take action if this value is null,
399
                        // we refresh the entity or its an uninitialized proxy.
400 191
                        if (isset($nonemptyComponents[$dqlAlias])) {
401 103
                            $element = $this->getEntity($data, $dqlAlias);
402
403 103
                            $association->setValue($parentObject, $element);
404 103
                            $this->uow->setOriginalEntityProperty($oid, $relationField, $element);
405
406 103
                            $mappedBy    = $association->getMappedBy();
407 103
                            $targetClass = $this->metadataCache[$association->getTargetEntity()];
408
409 103
                            if ($association->isOwningSide()) {
410
                                // TODO: Just check hints['fetched'] here?
411
                                // If there is an inverse mapping on the target class its bidirectional
412 42
                                if ($association->getInversedBy()) {
413 27
                                    $inverseAssociation = $targetClass->getProperty($association->getInversedBy());
414
415 27
                                    if ($inverseAssociation instanceof ToOneAssociationMetadata) {
416 8
                                        $inverseAssociation->setValue($element, $parentObject);
417
418 8
                                        $this->uow->setOriginalEntityProperty(
419 8
                                            spl_object_id($element),
420 8
                                            $inverseAssociation->getName(),
421 42
                                            $parentObject
422
                                        );
423
                                    }
424
                                }
425
                            } else {
426
                                // For sure bidirectional, as there is no inverse side in unidirectional mappings
427 61
                                $inverseAssociation = $targetClass->getProperty($mappedBy);
428
429 61
                                $inverseAssociation->setValue($element, $parentObject);
430
431 61
                                $this->uow->setOriginalEntityProperty(
432 61
                                    spl_object_id($element),
433 61
                                    $mappedBy,
434 61
                                    $parentObject
435
                                );
436
                            }
437
438
                            // Update result pointer
439 103
                            $this->resultPointers[$dqlAlias] = $element;
440
                        } else {
441 106
                            $association->setValue($parentObject, null);
442
443 191
                            $this->uow->setOriginalEntityProperty($oid, $relationField, null);
444
                        }
445
                    // else leave $reflFieldValue null for single-valued associations
446
                    } else {
447
                        // Update result pointer
448 282
                        $this->resultPointers[$dqlAlias] = $reflFieldValue;
449
                    }
450
                }
451
            } else {
452
                // PATH C: Its a root result element
453 545
                $this->rootAliases[$dqlAlias] = true; // Mark as root alias
454 545
                $entityKey                    = $this->rsm->entityMappings[$dqlAlias] ?: 0;
455
456
                // if this row has a NULL value for the root result id then make it a null result.
457 545
                if (! isset($nonemptyComponents[$dqlAlias])) {
458 6
                    if ($this->rsm->isMixed) {
459 2
                        $result[] = [$entityKey => null];
460
                    } else {
461 4
                        $result[] = null;
462
                    }
463 6
                    $resultKey = $this->resultCounter;
464 6
                    ++$this->resultCounter;
465 6
                    continue;
466
                }
467
468
                // check for existing result from the iterations before
469 545
                if (! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) {
470 545
                    $element = $this->getEntity($data, $dqlAlias);
471
472 545
                    if ($this->rsm->isMixed) {
473 39
                        $element = [$entityKey => $element];
474
                    }
475
476 545
                    if (isset($this->rsm->indexByMap[$dqlAlias])) {
477 22
                        $resultKey = $row[$this->rsm->indexByMap[$dqlAlias]];
478
479 22
                        if (isset($this->hints['collection'])) {
480 10
                            $this->hints['collection']->hydrateSet($resultKey, $element);
481
                        }
482
483 22
                        $result[$resultKey] = $element;
484
                    } else {
485 532
                        $resultKey = $this->resultCounter;
486 532
                        ++$this->resultCounter;
487
488 532
                        if (isset($this->hints['collection'])) {
489 100
                            $this->hints['collection']->hydrateAdd($element);
490
                        }
491
492 532
                        $result[] = $element;
493
                    }
494
495 545
                    $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey;
496
497
                    // Update result pointer
498 545
                    $this->resultPointers[$dqlAlias] = $element;
499
                } else {
500
                    // Update result pointer
501 73
                    $index                           = $this->identifierMap[$dqlAlias][$id[$dqlAlias]];
502 73
                    $this->resultPointers[$dqlAlias] = $result[$index];
503 73
                    $resultKey                       = $index;
504
                }
505
            }
506
507 545
            if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) {
508 545
                $this->uow->hydrationComplete();
509
            }
510
        }
511
512 578
        if (! isset($resultKey)) {
513 33
            $this->resultCounter++;
514
        }
515
516
        // Append scalar values to mixed result sets
517 578
        if (isset($rowData['scalars'])) {
518 52
            if (! isset($resultKey)) {
519 22
                $resultKey = (isset($this->rsm->indexByMap['scalars']))
520 2
                    ? $row[$this->rsm->indexByMap['scalars']]
521 22
                    : $this->resultCounter - 1;
522
            }
523
524 52
            foreach ($rowData['scalars'] as $name => $value) {
525 52
                $result[$resultKey][$name] = $value;
526
            }
527
        }
528
529
        // Append new object to mixed result sets
530 578
        if (isset($rowData['newObjects'])) {
531 17
            if (! isset($resultKey)) {
532 11
                $resultKey = $this->resultCounter - 1;
533
            }
534
535 17
            $hasNoScalars = ! (isset($rowData['scalars']) && $rowData['scalars']);
536
537 17
            foreach ($rowData['newObjects'] as $objIndex => $newObject) {
538 17
                $class = $newObject['class'];
539 17
                $args  = $newObject['args'];
540 17
                $obj   = $class->newInstanceArgs($args);
541
542 17
                if ($hasNoScalars && \count($rowData['newObjects']) === 1) {
543 8
                    $result[$resultKey] = $obj;
544
545 8
                    continue;
546
                }
547
548 9
                $result[$resultKey][$objIndex] = $obj;
549
            }
550
        }
551 578
    }
552
553
    /**
554
     * When executed in a hydrate() loop we may have to clear internal state to
555
     * decrease memory consumption.
556
     *
557
     * @param mixed $eventArgs
558
     */
559 2
    public function onClear($eventArgs)
560
    {
561 2
        parent::onClear($eventArgs);
562
563 2
        $aliases = array_keys($this->identifierMap);
564
565 2
        $this->identifierMap = array_fill_keys($aliases, []);
566 2
    }
567
}
568