Failed Conditions
Pull Request — master (#7885)
by Šimon
08:25
created

AbstractHydrator::gatherRowData()   B

Complexity

Conditions 11
Paths 10

Size

Total Lines 64
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 11.0225

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 36
c 2
b 0
f 0
nc 10
nop 3
dl 0
loc 64
ccs 33
cts 35
cp 0.9429
crap 11.0225
rs 7.3166

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\DBAL\Driver\Statement;
8
use Doctrine\DBAL\FetchMode;
9
use Doctrine\DBAL\Platforms\AbstractPlatform;
10
use Doctrine\ORM\EntityManagerInterface;
11
use Doctrine\ORM\Events;
12
use Doctrine\ORM\Mapping\ClassMetadata;
13
use Doctrine\ORM\Query\ResultSetMapping;
14
use Doctrine\ORM\UnitOfWork;
15
use ReflectionClass;
16
use const E_USER_DEPRECATED;
17
use function array_map;
18
use function array_merge;
19
use function end;
20
use function in_array;
21
use function is_array;
22
use function trigger_error;
23
24
/**
25
 * Base class for all hydrators. A hydrator is a class that provides some form
26
 * of transformation of an SQL result set into another structure.
27
 */
28
abstract class AbstractHydrator
29
{
30
    /**
31
     * The ResultSetMapping.
32
     *
33
     * @var ResultSetMapping
34
     */
35
    protected $rsm;
36
37
    /**
38
     * The EntityManager instance.
39
     *
40
     * @var EntityManagerInterface
41
     */
42
    protected $em;
43
44
    /**
45
     * The dbms Platform instance.
46
     *
47
     * @var AbstractPlatform
48
     */
49
    protected $platform;
50
51
    /**
52
     * The UnitOfWork of the associated EntityManager.
53
     *
54
     * @var UnitOfWork
55
     */
56
    protected $uow;
57
58
    /**
59
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
60
     *
61
     * @var ClassMetadata[]
62
     */
63
    protected $metadataCache = [];
64
65
    /**
66
     * The cache used during row-by-row hydration.
67
     *
68
     * @var mixed[][]
69
     */
70
    protected $cache = [];
71
72
    /**
73
     * The statement that provides the data to hydrate.
74
     *
75
     * @var Statement
76
     */
77
    protected $stmt;
78
79
    /**
80
     * The query hints.
81
     *
82
     * @var mixed[]
83
     */
84
    protected $hints;
85
86
    /**
87
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
88
     *
89
     * @param EntityManagerInterface $em The EntityManager to use.
90
     */
91 993
    public function __construct(EntityManagerInterface $em)
92
    {
93 993
        $this->em       = $em;
94 993
        $this->platform = $em->getConnection()->getDatabasePlatform();
95 993
        $this->uow      = $em->getUnitOfWork();
96 993
    }
97
98
    /**
99
     * Initiates a row-by-row hydration.
100
     *
101
     * @deprecated
102
     *
103
     * @param object  $stmt
104
     * @param object  $resultSetMapping
105
     * @param mixed[] $hints
106
     *
107
     * @return IterableResult
108
     */
109 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
110
    {
111 12
        @trigger_error(
112 12
            'Method ' . __METHOD__ . '() is deprecated and will be removed in Doctrine ORM 3.0. Use getIterable() instead.',
113 12
            E_USER_DEPRECATED
114
        );
115
116 12
        $this->stmt  = $stmt;
117 12
        $this->rsm   = $resultSetMapping;
118 12
        $this->hints = $hints;
119
120 12
        $evm = $this->em->getEventManager();
121
122 12
        $evm->addEventListener([Events::onClear], $this);
123
124 12
        $this->prepare();
125
126 12
        return new IterableResult($this);
0 ignored issues
show
Deprecated Code introduced by
The class Doctrine\ORM\Internal\Hydration\IterableResult has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

126
        return /** @scrutinizer ignore-deprecated */ new IterableResult($this);
Loading history...
127
    }
128
129
    /**
130
     * Initiates a row-by-row hydration.
131
     *
132
     * @param mixed[] $hints
133
     *
134
     * @return iterable<mixed>
135
     */
136 26
    public function getIterable(
137
        Statement $stmt,
138
        ResultSetMapping $resultSetMapping,
139
        array $hints = []
140
    ) : iterable {
141 26
        $this->stmt  = $stmt;
142 26
        $this->rsm   = $resultSetMapping;
143 26
        $this->hints = $hints;
144
145 26
        $evm = $this->em->getEventManager();
146
147 26
        $evm->addEventListener([Events::onClear], $this);
148
149 26
        $this->prepare();
150
151 26
        $result = [];
152
153 26
        while (true) {
154 26
            $row = $this->stmt->fetch(FetchMode::ASSOCIATIVE);
155
156 26
            if ($row === false || $row === null) {
157 24
                $this->cleanup();
158
159 24
                break;
160
            }
161
162 24
            $this->hydrateRowData($row, $result);
163
164 24
            if (is_array($result)) {
165 24
                yield end($result);
166
167 22
                continue;
168
            }
169
170
            yield $result;
171
        }
172 24
    }
173
174
    /**
175
     * Hydrates all rows returned by the passed statement instance at once.
176
     *
177
     * @param object  $stmt
178
     * @param object  $resultSetMapping
179
     * @param mixed[] $hints
180
     *
181
     * @return mixed[]
182
     */
183 979
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
184
    {
185 979
        $this->stmt  = $stmt;
186 979
        $this->rsm   = $resultSetMapping;
187 979
        $this->hints = $hints;
188
189 979
        $this->em->getEventManager()->addEventListener([Events::onClear], $this);
190
191 979
        $this->prepare();
192
193 978
        $result = $this->hydrateAllData();
194
195 968
        $this->cleanup();
196
197 968
        return $result;
198
    }
199
200
    /**
201
     * Hydrates a single row returned by the current statement instance during
202
     * row-by-row hydration with {@link iterate()} or {@link getIterable()}.
203
     *
204
     * @return mixed
205
     */
206 11
    public function hydrateRow()
207
    {
208 11
        $row = $this->stmt->fetch(FetchMode::ASSOCIATIVE);
209
210 11
        if ($row === false || $row === null) {
211 8
            $this->cleanup();
212
213 8
            return false;
214
        }
215
216 10
        $result = [];
217
218 10
        $this->hydrateRowData($row, $result);
219
220 10
        return $result;
221
    }
222
223
    /**
224
     * When executed in a hydrate() loop we have to clear internal state to
225
     * decrease memory consumption.
226
     *
227
     * @param mixed $eventArgs
228
     */
229 8
    public function onClear($eventArgs)
0 ignored issues
show
Unused Code introduced by
The parameter $eventArgs is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

229
    public function onClear(/** @scrutinizer ignore-unused */ $eventArgs)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
230
    {
231 8
    }
232
233
    /**
234
     * Executes one-time preparation tasks, once each time hydration is started
235
     * through {@link hydrateAll} or {@link iterate()}.
236
     */
237 106
    protected function prepare()
238
    {
239 106
    }
240
241
    /**
242
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
243
     * through {@link hydrateAll} or {@link iterate()}.
244
     */
245 978
    protected function cleanup()
246
    {
247 978
        $this->stmt->closeCursor();
248
249 978
        $this->stmt          = null;
250 978
        $this->rsm           = null;
251 978
        $this->cache         = [];
252 978
        $this->metadataCache = [];
253
254 978
        $this->em
255 978
            ->getEventManager()
256 978
            ->removeEventListener([Events::onClear], $this);
257 978
    }
258
259
    /**
260
     * Hydrates a single row from the current statement instance.
261
     *
262
     * Template method.
263
     *
264
     * @param mixed[] $data   The row data.
265
     * @param mixed[] $result The result to fill.
266
     *
267
     * @throws HydrationException
268
     */
269
    protected function hydrateRowData(array $data, array &$result)
0 ignored issues
show
Unused Code introduced by
The parameter $result is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

269
    protected function hydrateRowData(array $data, /** @scrutinizer ignore-unused */ array &$result)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
270
    {
271
        throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
272
    }
273
274
    /**
275
     * Hydrates all rows from the current statement instance at once.
276
     *
277
     * @return mixed[]
278
     */
279
    abstract protected function hydrateAllData();
280
281
    /**
282
     * Processes a row of the result set.
283
     *
284
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
285
     * Puts the elements of a result row into a new array, grouped by the dql alias
286
     * they belong to. The column names in the result set are mapped to their
287
     * field names during this procedure as well as any necessary conversions on
288
     * the values applied. Scalar values are kept in a specific key 'scalars'.
289
     *
290
     * @param mixed[] $data               SQL Result Row.
291
     * @param mixed[] $id                 Dql-Alias => ID-Hash.
292
     * @param mixed[] $nonemptyComponents Does this DQL-Alias has at least one non NULL value?
293
     *
294
     * @return mixed[] An array with all the fields (name => value) of the data row,
295
     *                grouped by their component alias.
296
     */
297 699
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
298
    {
299 699
        $rowData = ['data' => []];
300
301 699
        foreach ($data as $key => $value) {
302 699
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
303 699
            if ($cacheKeyInfo === null) {
304 9
                continue;
305
            }
306
307 699
            $fieldName = $cacheKeyInfo['fieldName'];
308
309
            switch (true) {
310 699
                case isset($cacheKeyInfo['isNewObjectParameter']):
311 19
                    $argIndex = $cacheKeyInfo['argIndex'];
312 19
                    $objIndex = $cacheKeyInfo['objIndex'];
313 19
                    $type     = $cacheKeyInfo['type'];
314 19
                    $value    = $type->convertToPHPValue($value, $this->platform);
315
316 19
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
317 19
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
318 19
                    break;
319
320 686
                case isset($cacheKeyInfo['isScalar']):
321 113
                    $type  = $cacheKeyInfo['type'];
322 113
                    $value = $type->convertToPHPValue($value, $this->platform);
323
324 113
                    $rowData['scalars'][$fieldName] = $value;
325 113
                    break;
326
327
                //case (isset($cacheKeyInfo['isMetaColumn'])):
328
                default:
329 645
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
330 645
                    $type     = $cacheKeyInfo['type'];
331
332
                    // If there are field name collisions in the child class, then we need
333
                    // to only hydrate if we are looking at the correct discriminator value
334 645
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
335 645
                        && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
336
                    ) {
337 24
                        break;
338
                    }
339
340
                    // in an inheritance hierarchy the same field could be defined several times.
341
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
342
                    // Per definition it cannot be that a field is defined several times and has several values.
343 645
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
344
                        break;
345
                    }
346
347 645
                    $rowData['data'][$dqlAlias][$fieldName] = $type
348 645
                        ? $type->convertToPHPValue($value, $this->platform)
349
                        : $value;
350
351 645
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
352 645
                        $id[$dqlAlias] .= '|' . $value;
353
354 645
                        $nonemptyComponents[$dqlAlias] = true;
355
                    }
356 645
                    break;
357
            }
358
        }
359
360 699
        return $rowData;
361
    }
362
363
    /**
364
     * Processes a row of the result set.
365
     *
366
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
367
     * simply converts column names to field names and properly converts the
368
     * values according to their types. The resulting row has the same number
369
     * of elements as before.
370
     *
371
     * @param mixed[] $data
372
     *
373
     * @return mixed[] The processed row.
374
     */
375 98
    protected function gatherScalarRowData(&$data)
376
    {
377 98
        $rowData = [];
378
379 98
        foreach ($data as $key => $value) {
380 98
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
381 98
            if ($cacheKeyInfo === null) {
382 1
                continue;
383
            }
384
385 98
            $fieldName = $cacheKeyInfo['fieldName'];
386
387
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
388
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
389 98
            if (! isset($cacheKeyInfo['isScalar'])) {
390 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
391 49
                $type      = $cacheKeyInfo['type'];
392 49
                $fieldName = $dqlAlias . '_' . $fieldName;
393 49
                $value     = $type
394 49
                    ? $type->convertToPHPValue($value, $this->platform)
395 49
                    : $value;
396
            }
397
398 98
            $rowData[$fieldName] = $value;
399
        }
400
401 98
        return $rowData;
402
    }
403
404
    /**
405
     * Retrieve column information from ResultSetMapping.
406
     *
407
     * @param string $key Column name
408
     *
409
     * @return mixed[]|null
410
     */
411 933
    protected function hydrateColumnInfo($key)
412
    {
413 933
        if (isset($this->cache[$key])) {
414 425
            return $this->cache[$key];
415
        }
416
417
        switch (true) {
418
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
419 933
            case isset($this->rsm->fieldMappings[$key]):
420 855
                $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
421 855
                $fieldName     = $this->rsm->fieldMappings[$key];
422 855
                $ownerMap      = $this->rsm->columnOwnerMap[$key];
423 855
                $property      = $classMetadata->getProperty($fieldName);
424
425
                $columnInfo = [
426 855
                    'isIdentifier' => $property->isPrimaryKey(),
427 855
                    'fieldName'    => $fieldName,
428 855
                    'type'         => $property->getType(),
0 ignored issues
show
Bug introduced by
The method getType() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

428
                    'type'         => $property->/** @scrutinizer ignore-call */ getType(),
Loading history...
429 855
                    'dqlAlias'     => $this->rsm->columnOwnerMap[$key],
430
                ];
431
432
                // the current discriminator value must be saved in order to disambiguate fields hydration,
433
                // should there be field name collisions
434 855
                if ($classMetadata->getParent() && isset($this->rsm->discriminatorColumns[$ownerMap])) {
435 108
                    return $this->cache[$key] = array_merge(
436 108
                        $columnInfo,
437
                        [
438 108
                            'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
439 108
                            'discriminatorValue'  => $classMetadata->discriminatorValue,
440 108
                            'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
441
                        ]
442
                    );
443
                }
444
445 826
                return $this->cache[$key] = $columnInfo;
446 743
            case isset($this->rsm->newObjectMappings[$key]):
447
                // WARNING: A NEW object is also a scalar, so it must be declared before!
448 19
                $mapping = $this->rsm->newObjectMappings[$key];
449
450 19
                return $this->cache[$key] = [
451 19
                    'isScalar'             => true,
452
                    'isNewObjectParameter' => true,
453 19
                    'fieldName'            => $this->rsm->scalarMappings[$key],
454 19
                    'type'                 => $this->rsm->typeMappings[$key],
455 19
                    'argIndex'             => $mapping['argIndex'],
456 19
                    'objIndex'             => $mapping['objIndex'],
457 19
                    'class'                => new ReflectionClass($mapping['className']),
458
                ];
459 730
            case isset($this->rsm->scalarMappings[$key]):
460 163
                return $this->cache[$key] = [
461 163
                    'isScalar'  => true,
462 163
                    'fieldName' => $this->rsm->scalarMappings[$key],
463 163
                    'type'      => $this->rsm->typeMappings[$key],
464
                ];
465 614
            case isset($this->rsm->metaMappings[$key]):
466
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
467 609
                $fieldName = $this->rsm->metaMappings[$key];
468 609
                $dqlAlias  = $this->rsm->columnOwnerMap[$key];
469
470
                // Cache metadata fetch
471 609
                $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
472
473 609
                return $this->cache[$key] = [
474 609
                    'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
475
                    'isMetaColumn' => true,
476 609
                    'fieldName'    => $fieldName,
477 609
                    'type'         => $this->rsm->typeMappings[$key],
478 609
                    'dqlAlias'     => $dqlAlias,
479
                ];
480
        }
481
482
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
483
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
484 11
        return null;
485
    }
486
487
    /**
488
     * @return string[]
489
     */
490 108
    private function getDiscriminatorValues(ClassMetadata $classMetadata) : array
491
    {
492 108
        $values = array_map(
493
            function (string $subClass) : string {
494 50
                return (string) $this->getClassMetadata($subClass)->discriminatorValue;
495 108
            },
496 108
            $classMetadata->getSubClasses()
497
        );
498
499 108
        $values[] = (string) $classMetadata->discriminatorValue;
500
501 108
        return $values;
502
    }
503
504
    /**
505
     * Retrieve ClassMetadata associated to entity class name.
506
     *
507
     * @param string $className
508
     *
509
     * @return ClassMetadata
510
     */
511 876
    protected function getClassMetadata($className)
512
    {
513 876
        if (! isset($this->metadataCache[$className])) {
514 876
            $this->metadataCache[$className] = $this->em->getClassMetadata($className);
515
        }
516
517 876
        return $this->metadataCache[$className];
518
    }
519
}
520