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

AbstractHydrator::gatherRowData()   B

Complexity

Conditions 11
Paths 10

Size

Total Lines 63
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 63
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
     * @param object  $stmt
102
     * @param object  $resultSetMapping
103
     * @param mixed[] $hints
104
     *
105
     * @return IterableResult
0 ignored issues
show
introduced by
Incorrect annotations group.
Loading history...
106
     * @deprecated
0 ignored issues
show
introduced by
Expected 0 lines after last content, found 1.
Loading history...
107
     *
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;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 16 spaces but found 17 spaces

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
353 645
                        $nonemptyComponents[$dqlAlias] = true;
354
                    }
355 645
                    break;
356
            }
357
        }
358
359 699
        return $rowData;
360
    }
361
362
    /**
363
     * Processes a row of the result set.
364
     *
365
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
366
     * simply converts column names to field names and properly converts the
367
     * values according to their types. The resulting row has the same number
368
     * of elements as before.
369
     *
370
     * @param mixed[] $data
371
     *
372
     * @return mixed[] The processed row.
373
     */
374 98
    protected function gatherScalarRowData(&$data)
375
    {
376 98
        $rowData = [];
377
378 98
        foreach ($data as $key => $value) {
379 98
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
380 98
            if ($cacheKeyInfo === null) {
381 1
                continue;
382
            }
383
384 98
            $fieldName = $cacheKeyInfo['fieldName'];
385
386
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
387
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
388 98
            if (! isset($cacheKeyInfo['isScalar'])) {
389 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
390 49
                $type      = $cacheKeyInfo['type'];
391 49
                $fieldName = $dqlAlias . '_' . $fieldName;
392 49
                $value     = $type
393 49
                    ? $type->convertToPHPValue($value, $this->platform)
394 49
                    : $value;
395
            }
396
397 98
            $rowData[$fieldName] = $value;
398
        }
399
400 98
        return $rowData;
401
    }
402
403
    /**
404
     * Retrieve column information from ResultSetMapping.
405
     *
406
     * @param string $key Column name
407
     *
408
     * @return mixed[]|null
409
     */
410 933
    protected function hydrateColumnInfo($key)
411
    {
412 933
        if (isset($this->cache[$key])) {
413 425
            return $this->cache[$key];
414
        }
415
416
        switch (true) {
417
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
418 933
            case isset($this->rsm->fieldMappings[$key]):
419 855
                $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
420 855
                $fieldName     = $this->rsm->fieldMappings[$key];
421 855
                $ownerMap      = $this->rsm->columnOwnerMap[$key];
422 855
                $property      = $classMetadata->getProperty($fieldName);
423
424
                $columnInfo = [
425 855
                    'isIdentifier' => $property->isPrimaryKey(),
426 855
                    'fieldName'    => $fieldName,
427 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

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