Passed
Pull Request — 2.6 (#7905)
by Luís
07:51
created

AbstractHydrator::hydrateRow()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 0
dl 0
loc 15
ccs 8
cts 8
cp 1
crap 2
rs 10
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 Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker;
27
use PDO;
28
use function array_map;
29
use function in_array;
30
31
/**
32
 * Base class for all hydrators. A hydrator is a class that provides some form
33
 * of transformation of an SQL result set into another structure.
34
 *
35
 * @since  2.0
36
 * @author Konsta Vesterinen <[email protected]>
37
 * @author Roman Borschel <[email protected]>
38
 * @author Guilherme Blanco <[email protected]>
39
 */
40
abstract class AbstractHydrator
41
{
42
    /**
43
     * The ResultSetMapping.
44
     *
45
     * @var \Doctrine\ORM\Query\ResultSetMapping
46
     */
47
    protected $_rsm;
48
49
    /**
50
     * The EntityManager instance.
51
     *
52
     * @var EntityManagerInterface
53
     */
54
    protected $_em;
55
56
    /**
57
     * The dbms Platform instance.
58
     *
59
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
60
     */
61
    protected $_platform;
62
63
    /**
64
     * The UnitOfWork of the associated EntityManager.
65
     *
66
     * @var \Doctrine\ORM\UnitOfWork
67
     */
68
    protected $_uow;
69
70
    /**
71
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
72
     *
73
     * @var array
74
     */
75
    protected $_metadataCache = [];
76
77
    /**
78
     * The cache used during row-by-row hydration.
79
     *
80
     * @var array
81
     */
82
    protected $_cache = [];
83
84
    /**
85
     * The statement that provides the data to hydrate.
86
     *
87
     * @var \Doctrine\DBAL\Driver\Statement
88
     */
89
    protected $_stmt;
90
91
    /**
92
     * The query hints.
93
     *
94
     * @var array
95
     */
96
    protected $_hints;
97
98
    /**
99
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
100
     *
101
     * @param EntityManagerInterface $em The EntityManager to use.
102
     */
103 1064
    public function __construct(EntityManagerInterface $em)
104
    {
105 1064
        $this->_em       = $em;
106 1064
        $this->_platform = $em->getConnection()->getDatabasePlatform();
107 1064
        $this->_uow      = $em->getUnitOfWork();
108 1064
    }
109
110
    /**
111
     * Initiates a row-by-row hydration.
112
     *
113
     * @param object $stmt
114
     * @param object $resultSetMapping
115
     * @param array  $hints
116
     *
117
     * @return IterableResult
118
     */
119 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
120
    {
121 12
        $this->_stmt  = $stmt;
122 12
        $this->_rsm   = $resultSetMapping;
123 12
        $this->_hints = $hints;
124
125 12
        $evm = $this->_em->getEventManager();
126
127 12
        $evm->addEventListener([Events::onClear], $this);
128
129 12
        $this->prepare();
130
131 12
        return new IterableResult($this);
132
    }
133
134
    /**
135
     * Hydrates all rows returned by the passed statement instance at once.
136
     *
137
     * @param object $stmt
138
     * @param object $resultSetMapping
139
     * @param array  $hints
140
     *
141
     * @return array
142
     */
143 1052
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
144
    {
145 1052
        $this->_stmt  = $stmt;
146 1052
        $this->_rsm   = $resultSetMapping;
147 1052
        $this->_hints = $hints;
148
149 1052
        $this->_em->getEventManager()->addEventListener([Events::onClear], $this);
150
151 1052
        $this->prepare();
152
153 1051
        $result = $this->hydrateAllData();
154
155 1041
        $this->cleanup();
156
157 1041
        return $result;
158
    }
159
160
    /**
161
     * Hydrates a single row returned by the current statement instance during
162
     * row-by-row hydration with {@link iterate()}.
163
     *
164
     * @return mixed
165
     */
166 11
    public function hydrateRow()
167
    {
168 11
        $row = $this->_stmt->fetch(PDO::FETCH_ASSOC);
169
170 11
        if ( ! $row) {
171 8
            $this->cleanup();
172
173 8
            return false;
174
        }
175
176 10
        $result = [];
177
178 10
        $this->hydrateRowData($row, $result);
179
180 10
        return $result;
181
    }
182
183
    /**
184
     * When executed in a hydrate() loop we have to clear internal state to
185
     * decrease memory consumption.
186
     *
187
     * @param mixed $eventArgs
188
     *
189
     * @return void
190
     */
191 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

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

238
    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...
239
    {
240
        throw new HydrationException("hydrateRowData() not implemented by this hydrator.");
241
    }
242
243
    /**
244
     * Hydrates all rows from the current statement instance at once.
245
     *
246
     * @return array
247
     */
248
    abstract protected function hydrateAllData();
249
250
    /**
251
     * Processes a row of the result set.
252
     *
253
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
254
     * Puts the elements of a result row into a new array, grouped by the dql alias
255
     * they belong to. The column names in the result set are mapped to their
256
     * field names during this procedure as well as any necessary conversions on
257
     * the values applied. Scalar values are kept in a specific key 'scalars'.
258
     *
259
     * @param array  $data               SQL Result Row.
260
     * @param array &$id                 Dql-Alias => ID-Hash.
261
     * @param array &$nonemptyComponents Does this DQL-Alias has at least one non NULL value?
262
     *
263
     * @return array  An array with all the fields (name => value) of the data row,
264
     *                grouped by their component alias.
265
     */
266 736
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
267
    {
268 736
        $rowData = ['data' => []];
269
270 736
        foreach ($data as $key => $value) {
271 736
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
272 8
                continue;
273
            }
274
275 736
            $fieldName = $cacheKeyInfo['fieldName'];
276
277
            switch (true) {
278 736
                case (isset($cacheKeyInfo['isNewObjectParameter'])):
279 21
                    $argIndex = $cacheKeyInfo['argIndex'];
280 21
                    $objIndex = $cacheKeyInfo['objIndex'];
281 21
                    $type     = $cacheKeyInfo['type'];
282 21
                    $value    = $type->convertToPHPValue($value, $this->_platform);
283
284 21
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
285 21
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
286 21
                    break;
287
288 721
                case (isset($cacheKeyInfo['isScalar'])):
289 114
                    $type  = $cacheKeyInfo['type'];
290 114
                    $value = $type->convertToPHPValue($value, $this->_platform);
291
292 114
                    $rowData['scalars'][$fieldName] = $value;
293 114
                    break;
294
295
                //case (isset($cacheKeyInfo['isMetaColumn'])):
296
                default:
297 681
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
298 681
                    $type     = $cacheKeyInfo['type'];
299
300
                    // If there are field name collisions in the child class, then we need
301
                    // to only hydrate if we are looking at the correct discriminator value
302 681
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
303 681
                        && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
304
                    ) {
305 24
                        break;
306
                    }
307
308
                    // in an inheritance hierarchy the same field could be defined several times.
309
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
310
                    // Per definition it cannot be that a field is defined several times and has several values.
311 681
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
312
                        break;
313
                    }
314
315 681
                    $rowData['data'][$dqlAlias][$fieldName] = $type
316 681
                        ? $type->convertToPHPValue($value, $this->_platform)
317 1
                        : $value;
318
319 681
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
320 681
                        $id[$dqlAlias] .= '|' . $value;
321 681
                        $nonemptyComponents[$dqlAlias] = true;
322
                    }
323 736
                    break;
324
            }
325
        }
326
327 736
        return $rowData;
328
    }
329
330
    /**
331
     * Processes a row of the result set.
332
     *
333
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
334
     * simply converts column names to field names and properly converts the
335
     * values according to their types. The resulting row has the same number
336
     * of elements as before.
337
     *
338
     * @param array $data
339
     *
340
     * @return array The processed row.
341
     */
342 101
    protected function gatherScalarRowData(&$data)
343
    {
344 101
        $rowData = [];
345
346 101
        foreach ($data as $key => $value) {
347 101
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
348 1
                continue;
349
            }
350
351 101
            $fieldName = $cacheKeyInfo['fieldName'];
352
353 101
            if (isset($cacheKeyInfo['dqlAlias'])) {
354 51
                $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName;
355
            }
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 101
            if (! isset($cacheKeyInfo['isScalar'])) {
360 73
                $type  = $cacheKeyInfo['type'];
361 73
                $value = $type ? $type->convertToPHPValue($value, $this->_platform) : $value;
362
            }
363
364 101
            $rowData[$fieldName] = $value;
365
        }
366
367 101
        return $rowData;
368
    }
369
370
    /**
371
     * Retrieve column information from ResultSetMapping.
372
     *
373
     * @param string $key Column name
374
     *
375
     * @return array|null
376
     */
377 999
    protected function hydrateColumnInfo($key)
378
    {
379 999
        if (isset($this->_cache[$key])) {
380 445
            return $this->_cache[$key];
381
        }
382
383
        switch (true) {
384
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
385 999
            case (isset($this->_rsm->fieldMappings[$key])):
386 921
                $classMetadata = $this->getClassMetadata($this->_rsm->declaringClasses[$key]);
387 921
                $fieldName     = $this->_rsm->fieldMappings[$key];
388 921
                $fieldMapping  = $classMetadata->fieldMappings[$fieldName];
389 921
                $ownerMap      = $this->_rsm->columnOwnerMap[$key];
390
                $columnInfo    = [
391 921
                    'isIdentifier' => \in_array($fieldName, $classMetadata->identifier, true),
392 921
                    'fieldName'    => $fieldName,
393 921
                    'type'         => Type::getType($fieldMapping['type']),
394 921
                    'dqlAlias'     => $ownerMap,
395
                ];
396
397
                // the current discriminator value must be saved in order to disambiguate fields hydration,
398
                // should there be field name collisions
399 921
                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...
400 108
                    return $this->_cache[$key] = \array_merge(
401 108
                        $columnInfo,
402
                        [
403 108
                            'discriminatorColumn' => $this->_rsm->discriminatorColumns[$ownerMap],
404 108
                            'discriminatorValue'  => $classMetadata->discriminatorValue,
405 108
                            'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
406
                        ]
407
                    );
408
                }
409
410 892
                return $this->_cache[$key] = $columnInfo;
411
412 786
            case (isset($this->_rsm->newObjectMappings[$key])):
413
                // WARNING: A NEW object is also a scalar, so it must be declared before!
414 21
                $mapping = $this->_rsm->newObjectMappings[$key];
415
416 21
                return $this->_cache[$key] = [
417 21
                    'isScalar'             => true,
418
                    'isNewObjectParameter' => true,
419 21
                    'fieldName'            => $this->_rsm->scalarMappings[$key],
420 21
                    'type'                 => Type::getType($this->_rsm->typeMappings[$key]),
421 21
                    'argIndex'             => $mapping['argIndex'],
422 21
                    'objIndex'             => $mapping['objIndex'],
423 21
                    'class'                => new \ReflectionClass($mapping['className']),
424
                ];
425
426 771
            case isset($this->_rsm->scalarMappings[$key], $this->_hints[LimitSubqueryWalker::FORCE_SCALAR_CONVERSION]):
427 22
                return $this->_cache[$key] = [
428 22
                    'fieldName' => $this->_rsm->scalarMappings[$key],
429 22
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
430
                ];
431 770
            case (isset($this->_rsm->scalarMappings[$key])):
432 145
                return $this->_cache[$key] = [
433 145
                    'isScalar'  => true,
434 145
                    'fieldName' => $this->_rsm->scalarMappings[$key],
435 145
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
436
                ];
437
438 653
            case (isset($this->_rsm->metaMappings[$key])):
439
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
440 648
                $fieldName = $this->_rsm->metaMappings[$key];
441 648
                $dqlAlias  = $this->_rsm->columnOwnerMap[$key];
442 648
                $type      = isset($this->_rsm->typeMappings[$key])
443 648
                    ? Type::getType($this->_rsm->typeMappings[$key])
444 648
                    : null;
445
446
                // Cache metadata fetch
447 648
                $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]);
448
449 648
                return $this->_cache[$key] = [
450 648
                    'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
451
                    'isMetaColumn' => true,
452 648
                    'fieldName'    => $fieldName,
453 648
                    'type'         => $type,
454 648
                    'dqlAlias'     => $dqlAlias,
455
                ];
456
        }
457
458
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
459
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
460 10
        return null;
461
    }
462
463
    /**
464
     * @return string[]
465
     */
466 108
    private function getDiscriminatorValues(ClassMetadata $classMetadata) : array
467
    {
468 108
        $values = array_map(
469
            function (string $subClass) : string {
470 51
                return (string) $this->getClassMetadata($subClass)->discriminatorValue;
471 108
            },
472 108
            $classMetadata->subClasses
473
        );
474
475 108
        $values[] = (string) $classMetadata->discriminatorValue;
476
477 108
        return $values;
478
    }
479
480
    /**
481
     * Retrieve ClassMetadata associated to entity class name.
482
     *
483
     * @param string $className
484
     *
485
     * @return \Doctrine\ORM\Mapping\ClassMetadata
486
     */
487 947
    protected function getClassMetadata($className)
488
    {
489 947
        if ( ! isset($this->_metadataCache[$className])) {
490 947
            $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
491
        }
492
493 947
        return $this->_metadataCache[$className];
494
    }
495
496
    /**
497
     * Register entity as managed in UnitOfWork.
498
     *
499
     * @param ClassMetadata $class
500
     * @param object        $entity
501
     * @param array         $data
502
     *
503
     * @return void
504
     *
505
     * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
506
     */
507 73
    protected function registerManaged(ClassMetadata $class, $entity, array $data)
508
    {
509 73
        if ($class->isIdentifierComposite) {
510 5
            $id = [];
511
512 5
            foreach ($class->identifier as $fieldName) {
513 5
                $id[$fieldName] = isset($class->associationMappings[$fieldName])
514 3
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
515 5
                    : $data[$fieldName];
516
            }
517
        } else {
518 68
            $fieldName = $class->identifier[0];
519
            $id        = [
520 68
                $fieldName => isset($class->associationMappings[$fieldName])
521
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
522 68
                    : $data[$fieldName]
523
            ];
524
        }
525
526 73
        $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data);
527 73
    }
528
}
529