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

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

233
    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...
234
    {
235 8
    }
236
237
    /**
238
     * Executes one-time preparation tasks, once each time hydration is started
239
     * through {@link hydrateAll} or {@link iterate()}.
240
     */
241 106
    protected function prepare()
242
    {
243 106
    }
244
245
    /**
246
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
247
     * through {@link hydrateAll} or {@link iterate()}.
248
     */
249 978
    protected function cleanup()
250
    {
251 978
        $this->stmt->closeCursor();
252
253 978
        $this->stmt          = null;
254 978
        $this->rsm           = null;
255 978
        $this->cache         = [];
256 978
        $this->metadataCache = [];
257
258 978
        $this->em
259 978
             ->getEventManager()
260 978
             ->removeEventListener([Events::onClear], $this);
261 978
    }
262
263
    /**
264
     * Hydrates a single row from the current statement instance.
265
     *
266
     * Template method.
267
     *
268
     * @param mixed[] $data   The row data.
269
     * @param mixed[] $result The result to fill.
270
     *
271
     * @throws HydrationException
272
     */
273
    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

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

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