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

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

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

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

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