Passed
Push — 2.6 ( 1f82a2...a736a3 )
by Marco
16:29 queued 08:32
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
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Internal\Hydration;
21
22
use Doctrine\DBAL\Types\Type;
23
use Doctrine\ORM\EntityManagerInterface;
24
use Doctrine\ORM\Events;
25
use Doctrine\ORM\Mapping\ClassMetadata;
26
use PDO;
27
use function array_map;
28
use function in_array;
29
30
/**
31
 * Base class for all hydrators. A hydrator is a class that provides some form
32
 * of transformation of an SQL result set into another structure.
33
 *
34
 * @since  2.0
35
 * @author Konsta Vesterinen <[email protected]>
36
 * @author Roman Borschel <[email protected]>
37
 * @author Guilherme Blanco <[email protected]>
38
 */
39
abstract class AbstractHydrator
40
{
41
    /**
42
     * The ResultSetMapping.
43
     *
44
     * @var \Doctrine\ORM\Query\ResultSetMapping
45
     */
46
    protected $_rsm;
47
48
    /**
49
     * The EntityManager instance.
50
     *
51
     * @var EntityManagerInterface
52
     */
53
    protected $_em;
54
55
    /**
56
     * The dbms Platform instance.
57
     *
58
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
59
     */
60
    protected $_platform;
61
62
    /**
63
     * The UnitOfWork of the associated EntityManager.
64
     *
65
     * @var \Doctrine\ORM\UnitOfWork
66
     */
67
    protected $_uow;
68
69
    /**
70
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
71
     *
72
     * @var array
73
     */
74
    protected $_metadataCache = [];
75
76
    /**
77
     * The cache used during row-by-row hydration.
78
     *
79
     * @var array
80
     */
81
    protected $_cache = [];
82
83
    /**
84
     * The statement that provides the data to hydrate.
85
     *
86
     * @var \Doctrine\DBAL\Driver\Statement
87
     */
88
    protected $_stmt;
89
90
    /**
91
     * The query hints.
92
     *
93
     * @var array
94
     */
95
    protected $_hints;
96
97
    /**
98
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
99
     *
100
     * @param EntityManagerInterface $em The EntityManager to use.
101
     */
102 1043
    public function __construct(EntityManagerInterface $em)
103
    {
104 1043
        $this->_em       = $em;
105 1043
        $this->_platform = $em->getConnection()->getDatabasePlatform();
106 1043
        $this->_uow      = $em->getUnitOfWork();
107 1043
    }
108
109
    /**
110
     * Initiates a row-by-row hydration.
111
     *
112
     * @param object $stmt
113
     * @param object $resultSetMapping
114
     * @param array  $hints
115
     *
116
     * @return IterableResult
117
     */
118 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
119
    {
120 12
        $this->_stmt  = $stmt;
121 12
        $this->_rsm   = $resultSetMapping;
122 12
        $this->_hints = $hints;
123
124 12
        $evm = $this->_em->getEventManager();
125
126 12
        $evm->addEventListener([Events::onClear], $this);
127
128 12
        $this->prepare();
129
130 12
        return new IterableResult($this);
131
    }
132
133
    /**
134
     * Hydrates all rows returned by the passed statement instance at once.
135
     *
136
     * @param object $stmt
137
     * @param object $resultSetMapping
138
     * @param array  $hints
139
     *
140
     * @return array
141
     */
142 1031
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
143
    {
144 1031
        $this->_stmt  = $stmt;
145 1031
        $this->_rsm   = $resultSetMapping;
146 1031
        $this->_hints = $hints;
147
148 1031
        $this->_em->getEventManager()->addEventListener([Events::onClear], $this);
149
150 1031
        $this->prepare();
151
152 1030
        $result = $this->hydrateAllData();
153
154 1020
        $this->cleanup();
155
156 1020
        return $result;
157
    }
158
159
    /**
160
     * Hydrates a single row returned by the current statement instance during
161
     * row-by-row hydration with {@link iterate()}.
162
     *
163
     * @return mixed
164
     */
165 11
    public function hydrateRow()
166
    {
167 11
        $row = $this->_stmt->fetch(PDO::FETCH_ASSOC);
168
169 11
        if ( ! $row) {
170 8
            $this->cleanup();
171
172 8
            return false;
173
        }
174
175 10
        $result = [];
176
177 10
        $this->hydrateRowData($row, $result);
178
179 10
        return $result;
180
    }
181
182
    /**
183
     * When executed in a hydrate() loop we have to clear internal state to
184
     * decrease memory consumption.
185
     *
186
     * @param mixed $eventArgs
187
     *
188
     * @return void
189
     */
190 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

190
    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...
191
    {
192 8
    }
193
194
    /**
195
     * Executes one-time preparation tasks, once each time hydration is started
196
     * through {@link hydrateAll} or {@link iterate()}.
197
     *
198
     * @return void
199
     */
200 106
    protected function prepare()
201
    {
202 106
    }
203
204
    /**
205
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
206
     * through {@link hydrateAll} or {@link iterate()}.
207
     *
208
     * @return void
209
     */
210 1028
    protected function cleanup()
211
    {
212 1028
        $this->_stmt->closeCursor();
213
214 1028
        $this->_stmt          = null;
215 1028
        $this->_rsm           = null;
216 1028
        $this->_cache         = [];
217 1028
        $this->_metadataCache = [];
218
219
        $this
220 1028
            ->_em
221 1028
            ->getEventManager()
222 1028
            ->removeEventListener([Events::onClear], $this);
223 1028
    }
224
225
    /**
226
     * Hydrates a single row from the current statement instance.
227
     *
228
     * Template method.
229
     *
230
     * @param array $data   The row data.
231
     * @param array $result The result to fill.
232
     *
233
     * @return void
234
     *
235
     * @throws HydrationException
236
     */
237
    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

237
    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...
238
    {
239
        throw new HydrationException("hydrateRowData() not implemented by this hydrator.");
240
    }
241
242
    /**
243
     * Hydrates all rows from the current statement instance at once.
244
     *
245
     * @return array
246
     */
247
    abstract protected function hydrateAllData();
248
249
    /**
250
     * Processes a row of the result set.
251
     *
252
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
253
     * Puts the elements of a result row into a new array, grouped by the dql alias
254
     * they belong to. The column names in the result set are mapped to their
255
     * field names during this procedure as well as any necessary conversions on
256
     * the values applied. Scalar values are kept in a specific key 'scalars'.
257
     *
258
     * @param array  $data               SQL Result Row.
259
     * @param array &$id                 Dql-Alias => ID-Hash.
260
     * @param array &$nonemptyComponents Does this DQL-Alias has at least one non NULL value?
261
     *
262
     * @return array  An array with all the fields (name => value) of the data row,
263
     *                grouped by their component alias.
264
     */
265 728
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
266
    {
267 728
        $rowData = ['data' => []];
268
269 728
        foreach ($data as $key => $value) {
270 728
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
271 8
                continue;
272
            }
273
274 728
            $fieldName = $cacheKeyInfo['fieldName'];
275
276
            switch (true) {
277 728
                case (isset($cacheKeyInfo['isNewObjectParameter'])):
278 21
                    $argIndex = $cacheKeyInfo['argIndex'];
279 21
                    $objIndex = $cacheKeyInfo['objIndex'];
280 21
                    $type     = $cacheKeyInfo['type'];
281 21
                    $value    = $type->convertToPHPValue($value, $this->_platform);
282
283 21
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
284 21
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
285 21
                    break;
286
287 713
                case (isset($cacheKeyInfo['isScalar'])):
288 112
                    $type  = $cacheKeyInfo['type'];
289 112
                    $value = $type->convertToPHPValue($value, $this->_platform);
290
291 112
                    $rowData['scalars'][$fieldName] = $value;
292 112
                    break;
293
294
                //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...
295
                default:
296 675
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
297 675
                    $type     = $cacheKeyInfo['type'];
298
299
                    // If there are field name collisions in the child class, then we need
300
                    // to only hydrate if we are looking at the correct discriminator value
301 675
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
302 675
                        && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
303
                    ) {
304 24
                        break;
305
                    }
306
307
                    // in an inheritance hierarchy the same field could be defined several times.
308
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
309
                    // Per definition it cannot be that a field is defined several times and has several values.
310 675
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
311
                        break;
312
                    }
313
314 675
                    $rowData['data'][$dqlAlias][$fieldName] = $type
315 675
                        ? $type->convertToPHPValue($value, $this->_platform)
316 1
                        : $value;
317
318 675
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
319 675
                        $id[$dqlAlias] .= '|' . $value;
320 675
                        $nonemptyComponents[$dqlAlias] = true;
321
                    }
322 728
                    break;
323
            }
324
        }
325
326 728
        return $rowData;
327
    }
328
329
    /**
330
     * Processes a row of the result set.
331
     *
332
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
333
     * simply converts column names to field names and properly converts the
334
     * values according to their types. The resulting row has the same number
335
     * of elements as before.
336
     *
337
     * @param array $data
338
     *
339
     * @return array The processed row.
340
     */
341 98
    protected function gatherScalarRowData(&$data)
342
    {
343 98
        $rowData = [];
344
345 98
        foreach ($data as $key => $value) {
346 98
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
347 1
                continue;
348
            }
349
350 98
            $fieldName = $cacheKeyInfo['fieldName'];
351
352
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
353
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
354 98
            if ( ! isset($cacheKeyInfo['isScalar'])) {
355 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
356 49
                $type      = $cacheKeyInfo['type'];
357 49
                $fieldName = $dqlAlias . '_' . $fieldName;
358 49
                $value     = $type
359 49
                    ? $type->convertToPHPValue($value, $this->_platform)
360 49
                    : $value;
361
            }
362
363 98
            $rowData[$fieldName] = $value;
364
        }
365
366 98
        return $rowData;
367
    }
368
369
    /**
370
     * Retrieve column information from ResultSetMapping.
371
     *
372
     * @param string $key Column name
373
     *
374
     * @return array|null
375
     */
376 981
    protected function hydrateColumnInfo($key)
377
    {
378 981
        if (isset($this->_cache[$key])) {
379 437
            return $this->_cache[$key];
380
        }
381
382
        switch (true) {
383
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
384 981
            case (isset($this->_rsm->fieldMappings[$key])):
385 905
                $classMetadata = $this->getClassMetadata($this->_rsm->declaringClasses[$key]);
386 905
                $fieldName     = $this->_rsm->fieldMappings[$key];
387 905
                $fieldMapping  = $classMetadata->fieldMappings[$fieldName];
388 905
                $ownerMap      = $this->_rsm->columnOwnerMap[$key];
389
                $columnInfo    = [
390 905
                    'isIdentifier' => \in_array($fieldName, $classMetadata->identifier, true),
391 905
                    'fieldName'    => $fieldName,
392 905
                    'type'         => Type::getType($fieldMapping['type']),
393 905
                    'dqlAlias'     => $ownerMap,
394
                ];
395
396
                // the current discriminator value must be saved in order to disambiguate fields hydration,
397
                // should there be field name collisions
398 905
                if ($classMetadata->parentClasses && isset($this->_rsm->discriminatorColumns[$ownerMap])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $classMetadata->parentClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
399 108
                    return $this->_cache[$key] = \array_merge(
400 108
                        $columnInfo,
401
                        [
402 108
                            'discriminatorColumn' => $this->_rsm->discriminatorColumns[$ownerMap],
403 108
                            'discriminatorValue'  => $classMetadata->discriminatorValue,
404 108
                            'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
405
                        ]
406
                    );
407
                }
408
409 876
                return $this->_cache[$key] = $columnInfo;
410
411 776
            case (isset($this->_rsm->newObjectMappings[$key])):
412
                // WARNING: A NEW object is also a scalar, so it must be declared before!
413 21
                $mapping = $this->_rsm->newObjectMappings[$key];
414
415 21
                return $this->_cache[$key] = [
416 21
                    'isScalar'             => true,
417
                    'isNewObjectParameter' => true,
418 21
                    'fieldName'            => $this->_rsm->scalarMappings[$key],
419 21
                    'type'                 => Type::getType($this->_rsm->typeMappings[$key]),
420 21
                    'argIndex'             => $mapping['argIndex'],
421 21
                    'objIndex'             => $mapping['objIndex'],
422 21
                    'class'                => new \ReflectionClass($mapping['className']),
423
                ];
424
425 761
            case (isset($this->_rsm->scalarMappings[$key])):
426 163
                return $this->_cache[$key] = [
427 163
                    'isScalar'  => true,
428 163
                    'fieldName' => $this->_rsm->scalarMappings[$key],
429 163
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
430
                ];
431
432 646
            case (isset($this->_rsm->metaMappings[$key])):
433
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
434 641
                $fieldName = $this->_rsm->metaMappings[$key];
435 641
                $dqlAlias  = $this->_rsm->columnOwnerMap[$key];
436 641
                $type      = isset($this->_rsm->typeMappings[$key])
437 641
                    ? Type::getType($this->_rsm->typeMappings[$key])
438 641
                    : null;
439
440
                // Cache metadata fetch
441 641
                $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]);
442
443 641
                return $this->_cache[$key] = [
444 641
                    'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
445
                    'isMetaColumn' => true,
446 641
                    'fieldName'    => $fieldName,
447 641
                    'type'         => $type,
448 641
                    '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 10
        return null;
455
    }
456
457
    /**
458
     * @return string[]
459
     */
460 108
    private function getDiscriminatorValues(ClassMetadata $classMetadata) : array
461
    {
462 108
        $values = array_map(
463 108
            function (string $subClass) : string {
464 51
                return (string) $this->getClassMetadata($subClass)->discriminatorValue;
465 108
            },
466 108
            $classMetadata->subClasses
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 \Doctrine\ORM\Mapping\ClassMetadata
480
     */
481 930
    protected function getClassMetadata($className)
482
    {
483 930
        if ( ! isset($this->_metadataCache[$className])) {
484 930
            $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
485
        }
486
487 930
        return $this->_metadataCache[$className];
488
    }
489
490
    /**
491
     * Register entity as managed in UnitOfWork.
492
     *
493
     * @param ClassMetadata $class
494
     * @param object        $entity
495
     * @param array         $data
496
     *
497
     * @return void
498
     *
499
     * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
500
     */
501 72
    protected function registerManaged(ClassMetadata $class, $entity, array $data)
502
    {
503 72
        if ($class->isIdentifierComposite) {
504 5
            $id = [];
505
506 5
            foreach ($class->identifier as $fieldName) {
507 5
                $id[$fieldName] = isset($class->associationMappings[$fieldName])
508 3
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
509 5
                    : $data[$fieldName];
510
            }
511
        } else {
512 67
            $fieldName = $class->identifier[0];
513
            $id        = [
514 67
                $fieldName => isset($class->associationMappings[$fieldName])
515
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
516 67
                    : $data[$fieldName]
517
            ];
518
        }
519
520 72
        $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data);
521 72
    }
522
}
523