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

AbstractHydrator::onClear()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

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

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

269
    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...
270
    {
271
        throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
272
    }
273
274
    /**
275
     * Hydrates all rows from the current statement instance at once.
276
     *
277
     * @return mixed[]
278
     */
279
    abstract protected function hydrateAllData();
280
281
    /**
282
     * Processes a row of the result set.
283
     *
284
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
285
     * Puts the elements of a result row into a new array, grouped by the dql alias
286
     * they belong to. The column names in the result set are mapped to their
287
     * field names during this procedure as well as any necessary conversions on
288
     * the values applied. Scalar values are kept in a specific key 'scalars'.
289
     *
290
     * @param mixed[] $data               SQL Result Row.
291
     * @param mixed[] $id                 Dql-Alias => ID-Hash.
292
     * @param mixed[] $nonemptyComponents Does this DQL-Alias has at least one non NULL value?
293
     *
294
     * @return mixed[] An array with all the fields (name => value) of the data row,
295
     *                grouped by their component alias.
296
     */
297 699
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
298
    {
299 699
        $rowData = ['data' => []];
300
301 699
        foreach ($data as $key => $value) {
302 699
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
303 699
            if ($cacheKeyInfo === null) {
304 9
                continue;
305
            }
306
307 699
            $fieldName = $cacheKeyInfo['fieldName'];
308
309
            switch (true) {
310 699
                case isset($cacheKeyInfo['isNewObjectParameter']):
311 19
                    $argIndex = $cacheKeyInfo['argIndex'];
312 19
                    $objIndex = $cacheKeyInfo['objIndex'];
313 19
                    $type     = $cacheKeyInfo['type'];
314 19
                    $value    = $type->convertToPHPValue($value, $this->platform);
315
316 19
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
317 19
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
318 19
                    break;
319
320 686
                case isset($cacheKeyInfo['isScalar']):
321 113
                    $type  = $cacheKeyInfo['type'];
322 113
                    $value = $type->convertToPHPValue($value, $this->platform);
323
324 113
                    $rowData['scalars'][$fieldName] = $value;
325 113
                    break;
326
327
                //case (isset($cacheKeyInfo['isMetaColumn'])):
328
                default:
329 645
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
330 645
                    $type     = $cacheKeyInfo['type'];
331
332
                    // If there are field name collisions in the child class, then we need
333
                    // to only hydrate if we are looking at the correct discriminator value
334 645
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
335 645
                        && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
336
                    ) {
337 24
                        break;
338
                    }
339
340
                    // in an inheritance hierarchy the same field could be defined several times.
341
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
342
                    // Per definition it cannot be that a field is defined several times and has several values.
343 645
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
344
                        break;
345
                    }
346
347 645
                    $rowData['data'][$dqlAlias][$fieldName] = $type
348 645
                        ? $type->convertToPHPValue($value, $this->platform)
349
                        : $value;
350
351 645
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
352 645
                        $id[$dqlAlias]                 .= '|' . $value;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 16 spaces but found 17 spaces

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
353 645
                        $nonemptyComponents[$dqlAlias] = true;
354
                    }
355 645
                    break;
356
            }
357
        }
358
359 699
        return $rowData;
360
    }
361
362
    /**
363
     * Processes a row of the result set.
364
     *
365
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
366
     * simply converts column names to field names and properly converts the
367
     * values according to their types. The resulting row has the same number
368
     * of elements as before.
369
     *
370
     * @param mixed[] $data
371
     *
372
     * @return mixed[] The processed row.
373
     */
374 98
    protected function gatherScalarRowData(&$data)
375
    {
376 98
        $rowData = [];
377
378 98
        foreach ($data as $key => $value) {
379 98
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
380 98
            if ($cacheKeyInfo === null) {
381 1
                continue;
382
            }
383
384 98
            $fieldName = $cacheKeyInfo['fieldName'];
385
386
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
387
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
388 98
            if (! isset($cacheKeyInfo['isScalar'])) {
389 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
390 49
                $type      = $cacheKeyInfo['type'];
391 49
                $fieldName = $dqlAlias . '_' . $fieldName;
392 49
                $value     = $type
393 49
                    ? $type->convertToPHPValue($value, $this->platform)
394 49
                    : $value;
395
            }
396
397 98
            $rowData[$fieldName] = $value;
398
        }
399
400 98
        return $rowData;
401
    }
402
403
    /**
404
     * Retrieve column information from ResultSetMapping.
405
     *
406
     * @param string $key Column name
407
     *
408
     * @return mixed[]|null
409
     */
410 933
    protected function hydrateColumnInfo($key)
411
    {
412 933
        if (isset($this->cache[$key])) {
413 425
            return $this->cache[$key];
414
        }
415
416
        switch (true) {
417
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
418 933
            case isset($this->rsm->fieldMappings[$key]):
419 855
                $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
420 855
                $fieldName     = $this->rsm->fieldMappings[$key];
421 855
                $ownerMap      = $this->rsm->columnOwnerMap[$key];
422 855
                $property      = $classMetadata->getProperty($fieldName);
423
424
                $columnInfo = [
425 855
                    'isIdentifier' => $property->isPrimaryKey(),
426 855
                    'fieldName'    => $fieldName,
427 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

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