Failed Conditions
Pull Request — master (#7885)
by Šimon
09:33
created

AbstractHydrator::gatherScalarRowData()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 5
nop 1
dl 0
loc 27
ccs 16
cts 16
cp 1
crap 5
rs 9.4555
c 0
b 0
f 0
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