Failed Conditions
Pull Request — master (#7885)
by Šimon
08:36
created

AbstractHydrator::hydrateAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 3
dl 0
loc 15
ccs 9
cts 9
cp 1
crap 1
rs 10
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 current;
0 ignored issues
show
introduced by
Type current is not used in this file.
Loading history...
17
use function end;
18
use function next;
0 ignored issues
show
introduced by
Type next is not used in this file.
Loading history...
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
use function array_map;
21
use function array_merge;
22
use function array_values;
0 ignored issues
show
introduced by
Type array_values is not used in this file.
Loading history...
23
use function in_array;
24
use function is_array;
25
use function trigger_error;
26
27
/**
28
 * Base class for all hydrators. A hydrator is a class that provides some form
29
 * of transformation of an SQL result set into another structure.
30
 */
31
abstract class AbstractHydrator
32
{
33
    /**
34
     * The ResultSetMapping.
35
     *
36
     * @var ResultSetMapping
37
     */
38
    protected $rsm;
39
40
    /**
41
     * The EntityManager instance.
42
     *
43
     * @var EntityManagerInterface
44
     */
45
    protected $em;
46
47
    /**
48
     * The dbms Platform instance.
49
     *
50
     * @var AbstractPlatform
51
     */
52
    protected $platform;
53
54
    /**
55
     * The UnitOfWork of the associated EntityManager.
56
     *
57
     * @var UnitOfWork
58
     */
59
    protected $uow;
60
61
    /**
62
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
63
     *
64
     * @var ClassMetadata[]
65
     */
66
    protected $metadataCache = [];
67
68
    /**
69
     * The cache used during row-by-row hydration.
70
     *
71
     * @var mixed[][]
72
     */
73
    protected $cache = [];
74
75
    /**
76
     * The statement that provides the data to hydrate.
77
     *
78
     * @var Statement
79
     */
80
    protected $stmt;
81
82
    /**
83
     * The query hints.
84
     *
85
     * @var mixed[]
86
     */
87
    protected $hints;
88
89
    /**
90
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
91
     *
92
     * @param EntityManagerInterface $em The EntityManager to use.
93
     */
94 993
    public function __construct(EntityManagerInterface $em)
95
    {
96 993
        $this->em       = $em;
97 993
        $this->platform = $em->getConnection()->getDatabasePlatform();
98 993
        $this->uow      = $em->getUnitOfWork();
99 993
    }
100
101
    /**
102
     * Initiates a row-by-row hydration.
103
     *
104
     * @deprecated
105
     *
106
     * @param object  $stmt
107
     * @param object  $resultSetMapping
108
     * @param mixed[] $hints
109
     *
110
     * @return IterableResult
111
     */
112 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
113
    {
114 12
        @trigger_error(
115 12
            'Method ' . __METHOD__ . '() is deprecated and will be removed in Doctrine ORM 3.0. Use getIterable() instead.',
116 12
            E_USER_DEPRECATED
117
        );
118
119 12
        $this->stmt  = $stmt;
120 12
        $this->rsm   = $resultSetMapping;
121 12
        $this->hints = $hints;
122
123 12
        $evm = $this->em->getEventManager();
124
125 12
        $evm->addEventListener([Events::onClear], $this);
126
127 12
        $this->prepare();
128
129 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

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

233
    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...
234
    {
235 8
    }
236
237
    /**
238
     * Executes one-time preparation tasks, once each time hydration is started
239
     * through {@link hydrateAll} or {@link iterate()}.
240
     */
241 106
    protected function prepare()
242
    {
243 106
    }
244
245
    /**
246
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
247
     * through {@link hydrateAll} or {@link iterate()}.
248
     */
249 978
    protected function cleanup()
250
    {
251 978
        $this->stmt->closeCursor();
252
253 978
        $this->stmt          = null;
254 978
        $this->rsm           = null;
255 978
        $this->cache         = [];
256 978
        $this->metadataCache = [];
257
258 978
        $this->em
259 978
             ->getEventManager()
260 978
             ->removeEventListener([Events::onClear], $this);
261 978
    }
262
263
    /**
264
     * Hydrates a single row from the current statement instance.
265
     *
266
     * Template method.
267
     *
268
     * @param mixed[] $data   The row data.
269
     * @param mixed[] $result The result to fill.
270
     *
271
     * @throws HydrationException
272
     */
273
    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

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

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