Passed
Pull Request — master (#7885)
by Šimon
05:58
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 array_values;
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 992
    public function __construct(EntityManagerInterface $em)
92
    {
93 992
        $this->em       = $em;
94 992
        $this->platform = $em->getConnection()->getDatabasePlatform();
95 992
        $this->uow      = $em->getUnitOfWork();
96 992
    }
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 25
    public function getIterable(
137
        Statement $stmt,
138
        ResultSetMapping $resultSetMapping,
139
        array $hints = []
140
    ) : iterable {
141 25
        $this->stmt  = $stmt;
142 25
        $this->rsm   = $resultSetMapping;
143 25
        $this->hints = $hints;
144
145 25
        $evm = $this->em->getEventManager();
146
147 25
        $evm->addEventListener([Events::onClear], $this);
148
149 25
        $this->prepare();
150
151 25
        while (true) {
152 25
            $row = $this->hydrateRow();
153 25
            if ($row === false) {
154 23
                break;
155
            }
156
157 23
            if (is_array($row)) {
158 23
                yield array_values($row)[0];
159
160 21
                continue;
161
            }
162
163
            yield $row;
164
        }
165 23
    }
166
167
    /**
168
     * Hydrates all rows returned by the passed statement instance at once.
169
     *
170
     * @param object  $stmt
171
     * @param object  $resultSetMapping
172
     * @param mixed[] $hints
173
     *
174
     * @return mixed[]
175
     */
176 979
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
177
    {
178 979
        $this->stmt  = $stmt;
179 979
        $this->rsm   = $resultSetMapping;
180 979
        $this->hints = $hints;
181
182 979
        $this->em->getEventManager()->addEventListener([Events::onClear], $this);
183
184 979
        $this->prepare();
185
186 978
        $result = $this->hydrateAllData();
187
188 968
        $this->cleanup();
189
190 968
        return $result;
191
    }
192
193
    /**
194
     * Hydrates a single row returned by the current statement instance during
195
     * row-by-row hydration with {@link iterate()} or {@link getIterable()}.
196
     *
197
     * @return mixed
198
     */
199 29
    public function hydrateRow()
200
    {
201 29
        $row = $this->stmt->fetch(FetchMode::ASSOCIATIVE);
202
203 29
        if ($row === false || $row === null) {
204 26
            $this->cleanup();
205
206 26
            return false;
207
        }
208
209 26
        $result = [];
210
211 26
        $this->hydrateRowData($row, $result);
212
213 26
        return $result;
214
    }
215
216
    /**
217
     * When executed in a hydrate() loop we have to clear internal state to
218
     * decrease memory consumption.
219
     *
220
     * @param mixed $eventArgs
221
     */
222 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

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

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

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