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

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

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

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

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