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

AbstractHydrator::gatherRowData()   B

Complexity

Conditions 11
Paths 10

Size

Total Lines 62
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 11.003

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 11
eloc 35
c 5
b 0
f 0
nop 3
dl 0
loc 62
ccs 33
cts 34
cp 0.9706
crap 11.003
rs 7.3166
nc 10

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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