Failed Conditions
Pull Request — master (#7885)
by Šimon
09:39
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 988
    public function __construct(EntityManagerInterface $em)
88
    {
89 988
        $this->em       = $em;
90 988
        $this->platform = $em->getConnection()->getDatabasePlatform();
91 988
        $this->uow      = $em->getUnitOfWork();
92 988
    }
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 15
    public function getIterable(
126
        Statement $stmt,
127
        ResultSetMapping $resultSetMapping,
128
        array $hints = []
129
    ) : IterableNewResult {
130 15
        $this->stmt  = $stmt;
131 15
        $this->rsm   = $resultSetMapping;
132 15
        $this->hints = $hints;
133
134 15
        $evm = $this->em->getEventManager();
135
136 15
        $evm->addEventListener([Events::onClear], $this);
137
138 15
        $this->prepare();
139
140 15
        return new IterableNewResult($this);
141
    }
142
143
    /**
144
     * Hydrates all rows returned by the passed statement instance at once.
145
     *
146
     * @param object  $stmt
147
     * @param object  $resultSetMapping
148
     * @param mixed[] $hints
149
     *
150
     * @return mixed[]
151
     */
152 976
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
153
    {
154 976
        $this->stmt  = $stmt;
155 976
        $this->rsm   = $resultSetMapping;
156 976
        $this->hints = $hints;
157
158 976
        $this->em->getEventManager()->addEventListener([Events::onClear], $this);
159
160 976
        $this->prepare();
161
162 975
        $result = $this->hydrateAllData();
163
164 965
        $this->cleanup();
165
166 965
        return $result;
167
    }
168
169
    /**
170
     * Hydrates a single row returned by the current statement instance during
171
     * row-by-row hydration with {@link iterate()}.
172
     *
173
     * @return mixed
174
     */
175 26
    public function hydrateRow()
176
    {
177 26
        $row = $this->stmt->fetch(FetchMode::ASSOCIATIVE);
178
179 26
        if (! $row) {
180 23
            $this->cleanup();
181
182 23
            return false;
183
        }
184
185 24
        $result = [];
186
187 24
        $this->hydrateRowData($row, $result);
188
189 24
        return $result;
190
    }
191
192
    /**
193
     * When executed in a hydrate() loop we have to clear internal state to
194
     * decrease memory consumption.
195
     *
196
     * @param mixed $eventArgs
197
     */
198 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

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

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

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