Completed
Push — master ( 92445d...0822f8 )
by Marco
23:45 queued 16:25
created

AbstractHydrator::getDiscriminatorValues()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 12
ccs 8
cts 8
cp 1
crap 1
rs 9.4285
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 function array_map;
16
use function array_merge;
17
use function in_array;
18
19
/**
20
 * Base class for all hydrators. A hydrator is a class that provides some form
21
 * of transformation of an SQL result set into another structure.
22
 */
23
abstract class AbstractHydrator
24
{
25
    /**
26
     * The ResultSetMapping.
27
     *
28
     * @var ResultSetMapping
29
     */
30
    protected $rsm;
31
32
    /**
33
     * The EntityManager instance.
34
     *
35
     * @var EntityManagerInterface
36
     */
37
    protected $em;
38
39
    /**
40
     * The dbms Platform instance.
41
     *
42
     * @var AbstractPlatform
43
     */
44
    protected $platform;
45
46
    /**
47
     * The UnitOfWork of the associated EntityManager.
48
     *
49
     * @var UnitOfWork
50
     */
51
    protected $uow;
52
53
    /**
54
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
55
     *
56
     * @var ClassMetadata[]
57
     */
58
    protected $metadataCache = [];
59
60
    /**
61
     * The cache used during row-by-row hydration.
62
     *
63
     * @var mixed[][]
64
     */
65
    protected $cache = [];
66
67
    /**
68
     * The statement that provides the data to hydrate.
69
     *
70
     * @var Statement
71
     */
72
    protected $stmt;
73
74
    /**
75
     * The query hints.
76
     *
77
     * @var mixed[]
78
     */
79
    protected $hints;
80
81
    /**
82
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
83
     *
84
     * @param EntityManagerInterface $em The EntityManager to use.
85
     */
86 988
    public function __construct(EntityManagerInterface $em)
87
    {
88 988
        $this->em       = $em;
89 988
        $this->platform = $em->getConnection()->getDatabasePlatform();
90 988
        $this->uow      = $em->getUnitOfWork();
91 988
    }
92
93
    /**
94
     * Initiates a row-by-row hydration.
95
     *
96
     * @param object  $stmt
97
     * @param object  $resultSetMapping
98
     * @param mixed[] $hints
99
     *
100
     * @return IterableResult
101
     */
102 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
103
    {
104 12
        $this->stmt  = $stmt;
105 12
        $this->rsm   = $resultSetMapping;
106 12
        $this->hints = $hints;
107
108 12
        $evm = $this->em->getEventManager();
109
110 12
        $evm->addEventListener([Events::onClear], $this);
111
112 12
        $this->prepare();
113
114 12
        return new IterableResult($this);
115
    }
116
117
    /**
118
     * Hydrates all rows returned by the passed statement instance at once.
119
     *
120
     * @param object  $stmt
121
     * @param object  $resultSetMapping
122
     * @param mixed[] $hints
123
     *
124
     * @return mixed[]
125
     */
126 976
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
127
    {
128 976
        $this->stmt  = $stmt;
129 976
        $this->rsm   = $resultSetMapping;
130 976
        $this->hints = $hints;
131
132 976
        $this->em->getEventManager()->addEventListener([Events::onClear], $this);
133
134 976
        $this->prepare();
135
136 975
        $result = $this->hydrateAllData();
137
138 965
        $this->cleanup();
139
140 965
        return $result;
141
    }
142
143
    /**
144
     * Hydrates a single row returned by the current statement instance during
145
     * row-by-row hydration with {@link iterate()}.
146
     *
147
     * @return mixed
148
     */
149 11
    public function hydrateRow()
150
    {
151 11
        $row = $this->stmt->fetch(FetchMode::ASSOCIATIVE);
152
153 11
        if (! $row) {
154 8
            $this->cleanup();
155
156 8
            return false;
157
        }
158
159 10
        $result = [];
160
161 10
        $this->hydrateRowData($row, $result);
162
163 10
        return $result;
164
    }
165
166
    /**
167
     * When executed in a hydrate() loop we have to clear internal state to
168
     * decrease memory consumption.
169
     *
170
     * @param mixed $eventArgs
171
     */
172 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

172
    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...
173
    {
174 8
    }
175
176
    /**
177
     * Executes one-time preparation tasks, once each time hydration is started
178
     * through {@link hydrateAll} or {@link iterate()}.
179
     */
180 106
    protected function prepare()
181
    {
182 106
    }
183
184
    /**
185
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
186
     * through {@link hydrateAll} or {@link iterate()}.
187
     */
188 973
    protected function cleanup()
189
    {
190 973
        $this->stmt->closeCursor();
191
192 973
        $this->stmt          = null;
193 973
        $this->rsm           = null;
194 973
        $this->cache         = [];
195 973
        $this->metadataCache = [];
196
197 973
        $this->em
198 973
             ->getEventManager()
199 973
             ->removeEventListener([Events::onClear], $this);
200 973
    }
201
202
    /**
203
     * Hydrates a single row from the current statement instance.
204
     *
205
     * Template method.
206
     *
207
     * @param mixed[] $data   The row data.
208
     * @param mixed[] $result The result to fill.
209
     *
210
     * @throws HydrationException
211
     */
212
    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

212
    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...
213
    {
214
        throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
215
    }
216
217
    /**
218
     * Hydrates all rows from the current statement instance at once.
219
     *
220
     * @return mixed[]
221
     */
222
    abstract protected function hydrateAllData();
223
224
    /**
225
     * Processes a row of the result set.
226
     *
227
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
228
     * Puts the elements of a result row into a new array, grouped by the dql alias
229
     * they belong to. The column names in the result set are mapped to their
230
     * field names during this procedure as well as any necessary conversions on
231
     * the values applied. Scalar values are kept in a specific key 'scalars'.
232
     *
233
     * @param mixed[] $data                SQL Result Row.
234
     * @param mixed[] &$id                 Dql-Alias => ID-Hash.
235
     * @param mixed[] &$nonemptyComponents Does this DQL-Alias has at least one non NULL value?
236
     *
237
     * @return mixed[] An array with all the fields (name => value) of the data row,
238
     *                grouped by their component alias.
239
     */
240 700
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
241
    {
242 700
        $rowData = ['data' => []];
243
244 700
        foreach ($data as $key => $value) {
245 700
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
246 700
            if ($cacheKeyInfo === null) {
247 8
                continue;
248
            }
249
250 700
            $fieldName = $cacheKeyInfo['fieldName'];
251
252
            switch (true) {
253 700
                case (isset($cacheKeyInfo['isNewObjectParameter'])):
254 19
                    $argIndex = $cacheKeyInfo['argIndex'];
255 19
                    $objIndex = $cacheKeyInfo['objIndex'];
256 19
                    $type     = $cacheKeyInfo['type'];
257 19
                    $value    = $type->convertToPHPValue($value, $this->platform);
258
259 19
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
260 19
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
261 19
                    break;
262
263 687
                case (isset($cacheKeyInfo['isScalar'])):
264 112
                    $type  = $cacheKeyInfo['type'];
265 112
                    $value = $type->convertToPHPValue($value, $this->platform);
266
267 112
                    $rowData['scalars'][$fieldName] = $value;
268 112
                    break;
269
270
                //case (isset($cacheKeyInfo['isMetaColumn'])):
0 ignored issues
show
Unused Code Comprehensibility introduced by
92% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
271
                default:
272 649
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
273 649
                    $type     = $cacheKeyInfo['type'];
274
275
                    // If there are field name collisions in the child class, then we need
276
                    // to only hydrate if we are looking at the correct discriminator value
277 649
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
278 649
                        && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
279
                    ) {
280 24
                        break;
281
                    }
282
283
                    // in an inheritance hierarchy the same field could be defined several times.
284
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
285
                    // Per definition it cannot be that a field is defined several times and has several values.
286 649
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
287
                        break;
288
                    }
289
290 649
                    $rowData['data'][$dqlAlias][$fieldName] = $type
291 649
                        ? $type->convertToPHPValue($value, $this->platform)
292
                        : $value;
293
294 649
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
295 649
                        $id[$dqlAlias]                .= '|' . $value;
296 649
                        $nonemptyComponents[$dqlAlias] = true;
297
                    }
298 700
                    break;
299
            }
300
        }
301
302 700
        return $rowData;
303
    }
304
305
    /**
306
     * Processes a row of the result set.
307
     *
308
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
309
     * simply converts column names to field names and properly converts the
310
     * values according to their types. The resulting row has the same number
311
     * of elements as before.
312
     *
313
     * @param mixed[] $data
314
     *
315
     * @return mixed[] The processed row.
316
     */
317 98
    protected function gatherScalarRowData(&$data)
318
    {
319 98
        $rowData = [];
320
321 98
        foreach ($data as $key => $value) {
322 98
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
323 98
            if ($cacheKeyInfo === null) {
324 1
                continue;
325
            }
326
327 98
            $fieldName = $cacheKeyInfo['fieldName'];
328
329
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
330
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
331 98
            if (! isset($cacheKeyInfo['isScalar'])) {
332 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
333 49
                $type      = $cacheKeyInfo['type'];
334 49
                $fieldName = $dqlAlias . '_' . $fieldName;
335 49
                $value     = $type
336 49
                    ? $type->convertToPHPValue($value, $this->platform)
337 49
                    : $value;
338
            }
339
340 98
            $rowData[$fieldName] = $value;
341
        }
342
343 98
        return $rowData;
344
    }
345
346
    /**
347
     * Retrieve column information from ResultSetMapping.
348
     *
349
     * @param string $key Column name
350
     *
351
     * @return mixed[]|null
352
     */
353 931
    protected function hydrateColumnInfo($key)
354
    {
355 931
        if (isset($this->cache[$key])) {
356 425
            return $this->cache[$key];
357
        }
358
359
        switch (true) {
360
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
361 931
            case (isset($this->rsm->fieldMappings[$key])):
362 855
                $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
363 855
                $fieldName     = $this->rsm->fieldMappings[$key];
364 855
                $ownerMap      = $this->rsm->columnOwnerMap[$key];
365 855
                $property      = $classMetadata->getProperty($fieldName);
366
367
                $columnInfo = [
368 855
                    'isIdentifier' => $property->isPrimaryKey(),
369 855
                    'fieldName'    => $fieldName,
370 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

370
                    'type'         => $property->/** @scrutinizer ignore-call */ getType(),
Loading history...
371 855
                    'dqlAlias'     => $this->rsm->columnOwnerMap[$key],
372
                ];
373
374
                // the current discriminator value must be saved in order to disambiguate fields hydration,
375
                // should there be field name collisions
376 855
                if ($classMetadata->getParent() && isset($this->rsm->discriminatorColumns[$ownerMap])) {
377 108
                    return $this->cache[$key] = array_merge(
378 108
                        $columnInfo,
379
                        [
380 108
                            'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
381 108
                            'discriminatorValue'  => $classMetadata->discriminatorValue,
382 108
                            'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
383
                        ]
384
                    );
385
                }
386
387 826
                return $this->cache[$key] = $columnInfo;
388
389 742
            case (isset($this->rsm->newObjectMappings[$key])):
390
                // WARNING: A NEW object is also a scalar, so it must be declared before!
391 19
                $mapping = $this->rsm->newObjectMappings[$key];
392
393 19
                return $this->cache[$key] = [
394 19
                    'isScalar'             => true,
395
                    'isNewObjectParameter' => true,
396 19
                    'fieldName'            => $this->rsm->scalarMappings[$key],
397 19
                    'type'                 => $this->rsm->typeMappings[$key],
398 19
                    'argIndex'             => $mapping['argIndex'],
399 19
                    'objIndex'             => $mapping['objIndex'],
400 19
                    'class'                => new \ReflectionClass($mapping['className']),
401
                ];
402
403 729
            case (isset($this->rsm->scalarMappings[$key])):
404 163
                return $this->cache[$key] = [
405 163
                    'isScalar'  => true,
406 163
                    'fieldName' => $this->rsm->scalarMappings[$key],
407 163
                    'type'      => $this->rsm->typeMappings[$key],
408
                ];
409
410 613
            case (isset($this->rsm->metaMappings[$key])):
411
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
412 608
                $fieldName = $this->rsm->metaMappings[$key];
413 608
                $dqlAlias  = $this->rsm->columnOwnerMap[$key];
414
415
                // Cache metadata fetch
416 608
                $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
417
418 608
                return $this->cache[$key] = [
419 608
                    'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
420
                    'isMetaColumn' => true,
421 608
                    'fieldName'    => $fieldName,
422 608
                    'type'         => $this->rsm->typeMappings[$key],
423 608
                    'dqlAlias'     => $dqlAlias,
424
                ];
425
        }
426
427
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
428
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
429 10
        return null;
430
    }
431
432
    /**
433
     * @return string[]
434
     */
435 108
    private function getDiscriminatorValues(ClassMetadata $classMetadata) : array
436
    {
437 108
        $values = array_map(
438 108
            function (string $subClass) : string {
439 51
                return (string) $this->getClassMetadata($subClass)->discriminatorValue;
440 108
            },
441 108
            $classMetadata->getSubClasses()
442
        );
443
444 108
        $values[] = (string) $classMetadata->discriminatorValue;
445
446 108
        return $values;
447
    }
448
449
    /**
450
     * Retrieve ClassMetadata associated to entity class name.
451
     *
452
     * @param string $className
453
     *
454
     * @return ClassMetadata
455
     */
456 876
    protected function getClassMetadata($className)
457
    {
458 876
        if (! isset($this->metadataCache[$className])) {
459 876
            $this->metadataCache[$className] = $this->em->getClassMetadata($className);
460
        }
461
462 876
        return $this->metadataCache[$className];
463
    }
464
}
465