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

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

203
    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...
204
    {
205 8
    }
206
207
    /**
208
     * Executes one-time preparation tasks, once each time hydration is started
209
     * through {@link hydrateAll} or {@link iterate()}.
210
     */
211 106
    protected function prepare()
212
    {
213 106
    }
214
215
    /**
216
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
217
     * through {@link hydrateAll} or {@link iterate()}.
218
     */
219 977
    protected function cleanup()
220
    {
221 977
        $this->stmt->closeCursor();
222
223 977
        $this->stmt          = null;
224 977
        $this->rsm           = null;
225 977
        $this->cache         = [];
226 977
        $this->metadataCache = [];
227
228 977
        $this->em
229 977
             ->getEventManager()
230 977
             ->removeEventListener([Events::onClear], $this);
231 977
    }
232
233
    /**
234
     * Hydrates a single row from the current statement instance.
235
     *
236
     * Template method.
237
     *
238
     * @param mixed[] $data   The row data.
239
     * @param mixed[] $result The result to fill.
240
     *
241
     * @throws HydrationException
242
     */
243
    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

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

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