Passed
Pull Request — master (#6709)
by Sergey
12:37
created

AbstractHydrator::gatherRowData()   D

Complexity

Conditions 14
Paths 40

Size

Total Lines 88
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 47
CRAP Score 14.0133

Importance

Changes 0
Metric Value
cc 14
eloc 50
nc 40
nop 3
dl 0
loc 88
rs 4.9516
c 0
b 0
f 0
ccs 47
cts 49
cp 0.9592
crap 14.0133

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\ORM\EntityManagerInterface;
8
use Doctrine\ORM\Events;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
use PDO;
11
12
/**
13
 * Base class for all hydrators. A hydrator is a class that provides some form
14
 * of transformation of an SQL result set into another structure.
15
 */
16
abstract class AbstractHydrator
17
{
18
    /**
19
     * The ResultSetMapping.
20
     *
21
     * @var \Doctrine\ORM\Query\ResultSetMapping
22
     */
23
    protected $rsm;
24
25
    /**
26
     * The EntityManager instance.
27
     *
28
     * @var EntityManagerInterface
29
     */
30
    protected $em;
31
32
    /**
33
     * The dbms Platform instance.
34
     *
35
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
36
     */
37
    protected $platform;
38
39
    /**
40
     * The UnitOfWork of the associated EntityManager.
41
     *
42
     * @var \Doctrine\ORM\UnitOfWork
43
     */
44
    protected $uow;
45
46
    /**
47
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
48
     *
49
     * @var ClassMetadata[]
50
     */
51
    protected $metadataCache = [];
52
53
    /**
54
     * The cache used during row-by-row hydration.
55
     *
56
     * @var mixed[][]
57
     */
58
    protected $cache = [];
59
60
    /**
61
     * The statement that provides the data to hydrate.
62
     *
63
     * @var \Doctrine\DBAL\Driver\Statement
64
     */
65
    protected $stmt;
66
67
    /**
68
     * The query hints.
69
     *
70
     * @var mixed[]
71
     */
72
    protected $hints;
73
74
    /**
75
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
76
     *
77
     * @param EntityManagerInterface $em The EntityManager to use.
78
     */
79 985
    public function __construct(EntityManagerInterface $em)
80
    {
81 985
        $this->em       = $em;
82 985
        $this->platform = $em->getConnection()->getDatabasePlatform();
83 985
        $this->uow      = $em->getUnitOfWork();
84 985
    }
85
86
    /**
87
     * Initiates a row-by-row hydration.
88
     *
89
     * @param object  $stmt
90
     * @param object  $resultSetMapping
91
     * @param mixed[] $hints
92
     *
93
     * @return IterableResult
94
     */
95 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
96
    {
97 12
        $this->stmt  = $stmt;
98 12
        $this->rsm   = $resultSetMapping;
99 12
        $this->hints = $hints;
100
101 12
        $evm = $this->em->getEventManager();
102
103 12
        $evm->addEventListener([Events::onClear], $this);
104
105 12
        $this->prepare();
106
107 12
        return new IterableResult($this);
108
    }
109
110
    /**
111
     * Hydrates all rows returned by the passed statement instance at once.
112
     *
113
     * @param object  $stmt
114
     * @param object  $resultSetMapping
115
     * @param mixed[] $hints
116
     *
117
     * @return mixed[]
118
     */
119 973
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
120
    {
121 973
        $this->stmt  = $stmt;
122 973
        $this->rsm   = $resultSetMapping;
123 973
        $this->hints = $hints;
124
125 973
        $this->em->getEventManager()->addEventListener([Events::onClear], $this);
126
127 973
        $this->prepare();
128
129 972
        $result = $this->hydrateAllData();
130
131 962
        $this->cleanup();
132
133 962
        return $result;
134
    }
135
136
    /**
137
     * Hydrates a single row returned by the current statement instance during
138
     * row-by-row hydration with {@link iterate()}.
139
     *
140
     * @return mixed
141
     */
142 11
    public function hydrateRow()
143
    {
144 11
        $row = $this->stmt->fetch(PDO::FETCH_ASSOC);
145
146 11
        if (! $row) {
147 8
            $this->cleanup();
148
149 8
            return false;
150
        }
151
152 10
        $result = [];
153
154 10
        $this->hydrateRowData($row, $result);
155
156 10
        return $result;
157
    }
158
159
    /**
160
     * When executed in a hydrate() loop we have to clear internal state to
161
     * decrease memory consumption.
162
     *
163
     * @param mixed $eventArgs
164
     */
165 8
    public function onClear($eventArgs)
166
    {
167 8
    }
168
169
    /**
170
     * Executes one-time preparation tasks, once each time hydration is started
171
     * through {@link hydrateAll} or {@link iterate()}.
172
     */
173 106
    protected function prepare()
174
    {
175 106
    }
176
177
    /**
178
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
179
     * through {@link hydrateAll} or {@link iterate()}.
180
     */
181 970
    protected function cleanup()
182
    {
183 970
        $this->stmt->closeCursor();
184
185 970
        $this->stmt          = null;
186 970
        $this->rsm           = null;
187 970
        $this->cache         = [];
188 970
        $this->metadataCache = [];
189
190 970
        $this->em
191 970
             ->getEventManager()
192 970
             ->removeEventListener([Events::onClear], $this);
193 970
    }
194
195
    /**
196
     * Hydrates a single row from the current statement instance.
197
     *
198
     * Template method.
199
     *
200
     * @param mixed[] $data   The row data.
201
     * @param mixed[] $result The result to fill.
202
     *
203
     * @throws HydrationException
204
     */
205
    protected function hydrateRowData(array $data, array &$result)
206
    {
207
        throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
208
    }
209
210
    /**
211
     * Hydrates all rows from the current statement instance at once.
212
     *
213
     * @return mixed[]
214
     */
215
    abstract protected function hydrateAllData();
216
217
    /**
218
     * Processes a row of the result set.
219
     *
220
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
221
     * Puts the elements of a result row into a new array, grouped by the dql alias
222
     * they belong to. The column names in the result set are mapped to their
223
     * field names during this procedure as well as any necessary conversions on
224
     * the values applied. Scalar values are kept in a specific key 'scalars'.
225
     *
226
     * @param mixed[] $data                SQL Result Row.
227
     * @param mixed[] &$id                 Dql-Alias => ID-Hash.
228
     * @param mixed[] &$nonemptyComponents Does this DQL-Alias has at least one non NULL value?
229
     *
230
     * @return mixed[] An array with all the fields (name => value) of the data row,
231
     *                grouped by their component alias.
232
     */
233 699
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
234
    {
235 699
        $rowData = ['data' => []];
236
237 699
        foreach ($data as $key => $value) {
238 699
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
239 699
            if ($cacheKeyInfo === null) {
240 8
                continue;
241
            }
242
243 699
            $fieldName = $cacheKeyInfo['fieldName'];
244
245
            switch (true) {
246 699
                case (isset($cacheKeyInfo['isNewObjectParameter'])):
247 20
                    $argIndex = $cacheKeyInfo['argIndex'];
248 20
                    $objIndex = $cacheKeyInfo['objIndex'];
249 20
                    $type     = $cacheKeyInfo['type'];
250 20
                    $value    = $type->convertToPHPValue($value, $this->platform);
251
252 20
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
253 20
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
254 20
                    break;
255
256 686
                case (isset($cacheKeyInfo['isScalar'])):
257 111
                    $type  = $cacheKeyInfo['type'];
258 111
                    $value = $type->convertToPHPValue($value, $this->platform);
259
260 111
                    $rowData['scalars'][$fieldName] = $value;
261 111
                    break;
262
263
                //case (isset($cacheKeyInfo['isMetaColumn'])):
0 ignored issues
show
Unused Code Comprehensibility introduced by
92% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
264
                default:
265 649
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
266 649
                    $type     = $cacheKeyInfo['type'];
267
268
                    // If there are field name collisions in the child class, then we need
269
                    // to only hydrate if we are looking at the correct discriminator value
270 649
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']]) &&
271
                        // Note: loose comparison required. See https://github.com/doctrine/doctrine2/pull/6304#issuecomment-323294442
272 649
                        $data[$cacheKeyInfo['discriminatorColumn']] != $cacheKeyInfo['discriminatorValue'] // TODO get rid of loose comparison
273
                    ) {
274 26
                        break;
275
                    }
276
277
                    // in an inheritance hierarchy the same field could be defined several times.
278
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
279
                    // Per definition it cannot be that a field is defined several times and has several values.
280 649
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
281
                        break;
282
                    }
283
284 649
                    $rowData['data'][$dqlAlias][$fieldName] = $type
285 649
                        ? $type->convertToPHPValue($value, $this->platform)
286
                        : $value;
287
288 649
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
289 649
                        $id[$dqlAlias]                .= '|' . $value;
290 649
                        $nonemptyComponents[$dqlAlias] = true;
291
                    }
292 699
                    break;
293
            }
294
        }
295
296 699
        foreach ($this->rsm->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) {
297 1
            $newObject = $rowData['newObjects'][$objIndex];
298 1
            unset($rowData['newObjects'][$objIndex]);
299
300 1
            $class  = $newObject['class'];
301 1
            $args   = $newObject['args'];
302 1
            $obj    = $class->newInstanceArgs($args);
303
304 1
            $rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
305
        }
306
307
308 699
        if (isset($rowData['newObjects'])) {
309 20
            foreach ($rowData['newObjects'] as $objIndex => $newObject) {
310 20
                $class  = $newObject['class'];
311 20
                $args   = $newObject['args'];
312 20
                ksort($args);
313 20
                $obj    = $class->newInstanceArgs($args);
314
315 20
                $rowData['newObjects'][$objIndex]['obj'] = $obj;
316
            }
317
        }
318
319
320 699
        return $rowData;
321
    }
322
323
    /**
324
     * Processes a row of the result set.
325
     *
326
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
327
     * simply converts column names to field names and properly converts the
328
     * values according to their types. The resulting row has the same number
329
     * of elements as before.
330
     *
331
     * @param mixed[] $data
332
     *
333
     * @return mixed[] The processed row.
334
     */
335 98
    protected function gatherScalarRowData(&$data)
336
    {
337 98
        $rowData = [];
338
339 98
        foreach ($data as $key => $value) {
340 98
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
341 98
            if ($cacheKeyInfo === null) {
342 1
                continue;
343
            }
344
345 98
            $fieldName = $cacheKeyInfo['fieldName'];
346
347
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
348
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
349 98
            if (! isset($cacheKeyInfo['isScalar'])) {
350 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
351 49
                $type      = $cacheKeyInfo['type'];
352 49
                $fieldName = $dqlAlias . '_' . $fieldName;
353 49
                $value     = $type
354 49
                    ? $type->convertToPHPValue($value, $this->platform)
355 49
                    : $value;
356
            }
357
358 98
            $rowData[$fieldName] = $value;
359
        }
360
361 98
        return $rowData;
362
    }
363
364
    /**
365
     * Retrieve column information from ResultSetMapping.
366
     *
367
     * @param string $key Column name
368
     *
369
     * @return mixed[]|null
370
     */
371 928
    protected function hydrateColumnInfo($key)
372
    {
373 928
        if (isset($this->cache[$key])) {
374 427
            return $this->cache[$key];
375
        }
376
377
        switch (true) {
378
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
379 928
            case (isset($this->rsm->fieldMappings[$key])):
380 853
                $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
381 853
                $fieldName     = $this->rsm->fieldMappings[$key];
382 853
                $ownerMap      = $this->rsm->columnOwnerMap[$key];
383 853
                $property      = $classMetadata->getProperty($fieldName);
384
385
                $columnInfo = [
386 853
                    'isIdentifier' => $property->isPrimaryKey(),
387 853
                    'fieldName'    => $fieldName,
388 853
                    'type'         => $property->getType(),
389 853
                    'dqlAlias'     => $this->rsm->columnOwnerMap[$key],
390
                ];
391
392
                // the current discriminator value must be saved in order to disambiguate fields hydration,
393
                // should there be field name collisions
394 853
                if ($classMetadata->getParent() && isset($this->rsm->discriminatorColumns[$ownerMap])) {
395 106
                    return $this->cache[$key] = \array_merge(
396 106
                        $columnInfo,
397
                        [
398 106
                            'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
399 106
                            'discriminatorValue'  => $classMetadata->discriminatorValue,
400
                        ]
401
                    );
402
                }
403
404 823
                return $this->cache[$key] = $columnInfo;
405
406 741
            case (isset($this->rsm->newObjectMappings[$key])):
407
                // WARNING: A NEW object is also a scalar, so it must be declared before!
408 20
                $mapping = $this->rsm->newObjectMappings[$key];
409
410 20
                return $this->cache[$key] = [
411 20
                    'isScalar'             => true,
412
                    'isNewObjectParameter' => true,
413 20
                    'fieldName'            => $this->rsm->scalarMappings[$key],
414 20
                    'type'                 => $this->rsm->typeMappings[$key],
415 20
                    'argIndex'             => $mapping['argIndex'],
416 20
                    'objIndex'             => $mapping['objIndex'],
417 20
                    'class'                => new \ReflectionClass($mapping['className']),
418
                ];
419
420 728
            case (isset($this->rsm->scalarMappings[$key])):
421 162
                return $this->cache[$key] = [
422 162
                    'isScalar'  => true,
423 162
                    'fieldName' => $this->rsm->scalarMappings[$key],
424 162
                    'type'      => $this->rsm->typeMappings[$key],
425
                ];
426
427 613
            case (isset($this->rsm->metaMappings[$key])):
428
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
429 608
                $fieldName = $this->rsm->metaMappings[$key];
430 608
                $dqlAlias  = $this->rsm->columnOwnerMap[$key];
431
432
                // Cache metadata fetch
433 608
                $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
434
435 608
                return $this->cache[$key] = [
436 608
                    'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
437
                    'isMetaColumn' => true,
438 608
                    'fieldName'    => $fieldName,
439 608
                    'type'         => $this->rsm->typeMappings[$key],
440 608
                    'dqlAlias'     => $dqlAlias,
441
                ];
442
        }
443
444
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
445
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
446 10
        return null;
447
    }
448
449
    /**
450
     * Retrieve ClassMetadata associated to entity class name.
451
     *
452
     * @param string $className
453
     *
454
     * @return \Doctrine\ORM\Mapping\ClassMetadata
455
     */
456 874
    protected function getClassMetadata($className)
457
    {
458 874
        if (! isset($this->metadataCache[$className])) {
459 874
            $this->metadataCache[$className] = $this->em->getClassMetadata($className);
460
        }
461
462 874
        return $this->metadataCache[$className];
463
    }
464
}
465