Failed Conditions
Pull Request — master (#7885)
by Šimon
11:22
created

AbstractHydrator::getClassMetadata()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
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 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