Completed
Push — 2.6 ( 6a827d...686f50 )
by Luís
29s queued 22s
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
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
354
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
355 101
            if (! isset($cacheKeyInfo['isScalar'])) {
356 73
                $type  = $cacheKeyInfo['type'];
357 73
                $value = $type ? $type->convertToPHPValue($value, $this->_platform) : $value;
358
359 73
                $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName;
360
            }
361
362 101
            $rowData[$fieldName] = $value;
363
        }
364
365 101
        return $rowData;
366
    }
367
368
    /**
369
     * Retrieve column information from ResultSetMapping.
370
     *
371
     * @param string $key Column name
372
     *
373
     * @return array|null
374
     */
375 999
    protected function hydrateColumnInfo($key)
376
    {
377 999
        if (isset($this->_cache[$key])) {
378 445
            return $this->_cache[$key];
379
        }
380
381
        switch (true) {
382
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
383 999
            case (isset($this->_rsm->fieldMappings[$key])):
384 921
                $classMetadata = $this->getClassMetadata($this->_rsm->declaringClasses[$key]);
385 921
                $fieldName     = $this->_rsm->fieldMappings[$key];
386 921
                $fieldMapping  = $classMetadata->fieldMappings[$fieldName];
387 921
                $ownerMap      = $this->_rsm->columnOwnerMap[$key];
388
                $columnInfo    = [
389 921
                    'isIdentifier' => \in_array($fieldName, $classMetadata->identifier, true),
390 921
                    'fieldName'    => $fieldName,
391 921
                    'type'         => Type::getType($fieldMapping['type']),
392 921
                    'dqlAlias'     => $ownerMap,
393
                ];
394
395
                // the current discriminator value must be saved in order to disambiguate fields hydration,
396
                // should there be field name collisions
397 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...
398 108
                    return $this->_cache[$key] = \array_merge(
399 108
                        $columnInfo,
400
                        [
401 108
                            'discriminatorColumn' => $this->_rsm->discriminatorColumns[$ownerMap],
402 108
                            'discriminatorValue'  => $classMetadata->discriminatorValue,
403 108
                            'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
404
                        ]
405
                    );
406
                }
407
408 892
                return $this->_cache[$key] = $columnInfo;
409
410 786
            case (isset($this->_rsm->newObjectMappings[$key])):
411
                // WARNING: A NEW object is also a scalar, so it must be declared before!
412 21
                $mapping = $this->_rsm->newObjectMappings[$key];
413
414 21
                return $this->_cache[$key] = [
415 21
                    'isScalar'             => true,
416
                    'isNewObjectParameter' => true,
417 21
                    'fieldName'            => $this->_rsm->scalarMappings[$key],
418 21
                    'type'                 => Type::getType($this->_rsm->typeMappings[$key]),
419 21
                    'argIndex'             => $mapping['argIndex'],
420 21
                    'objIndex'             => $mapping['objIndex'],
421 21
                    'class'                => new \ReflectionClass($mapping['className']),
422
                ];
423
424 771
            case isset($this->_rsm->scalarMappings[$key], $this->_hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]):
425 22
                return $this->_cache[$key] = [
426 22
                    'fieldName' => $this->_rsm->scalarMappings[$key],
427 22
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
428 22
                    'dqlAlias'  => '',
429
                ];
430 770
            case (isset($this->_rsm->scalarMappings[$key])):
431 145
                return $this->_cache[$key] = [
432 145
                    'isScalar'  => true,
433 145
                    'fieldName' => $this->_rsm->scalarMappings[$key],
434 145
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
435
                ];
436
437 653
            case (isset($this->_rsm->metaMappings[$key])):
438
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
439 648
                $fieldName = $this->_rsm->metaMappings[$key];
440 648
                $dqlAlias  = $this->_rsm->columnOwnerMap[$key];
441 648
                $type      = isset($this->_rsm->typeMappings[$key])
442 648
                    ? Type::getType($this->_rsm->typeMappings[$key])
443 648
                    : null;
444
445
                // Cache metadata fetch
446 648
                $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]);
447
448 648
                return $this->_cache[$key] = [
449 648
                    'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
450
                    'isMetaColumn' => true,
451 648
                    'fieldName'    => $fieldName,
452 648
                    'type'         => $type,
453 648
                    'dqlAlias'     => $dqlAlias,
454
                ];
455
        }
456
457
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
458
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
459 10
        return null;
460
    }
461
462
    /**
463
     * @return string[]
464
     */
465 108
    private function getDiscriminatorValues(ClassMetadata $classMetadata) : array
466
    {
467 108
        $values = array_map(
468
            function (string $subClass) : string {
469 51
                return (string) $this->getClassMetadata($subClass)->discriminatorValue;
470 108
            },
471 108
            $classMetadata->subClasses
472
        );
473
474 108
        $values[] = (string) $classMetadata->discriminatorValue;
475
476 108
        return $values;
477
    }
478
479
    /**
480
     * Retrieve ClassMetadata associated to entity class name.
481
     *
482
     * @param string $className
483
     *
484
     * @return \Doctrine\ORM\Mapping\ClassMetadata
485
     */
486 947
    protected function getClassMetadata($className)
487
    {
488 947
        if ( ! isset($this->_metadataCache[$className])) {
489 947
            $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
490
        }
491
492 947
        return $this->_metadataCache[$className];
493
    }
494
495
    /**
496
     * Register entity as managed in UnitOfWork.
497
     *
498
     * @param ClassMetadata $class
499
     * @param object        $entity
500
     * @param array         $data
501
     *
502
     * @return void
503
     *
504
     * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
505
     */
506 73
    protected function registerManaged(ClassMetadata $class, $entity, array $data)
507
    {
508 73
        if ($class->isIdentifierComposite) {
509 5
            $id = [];
510
511 5
            foreach ($class->identifier as $fieldName) {
512 5
                $id[$fieldName] = isset($class->associationMappings[$fieldName])
513 3
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
514 5
                    : $data[$fieldName];
515
            }
516
        } else {
517 68
            $fieldName = $class->identifier[0];
518
            $id        = [
519 68
                $fieldName => isset($class->associationMappings[$fieldName])
520
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
521 68
                    : $data[$fieldName]
522
            ];
523
        }
524
525 73
        $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data);
526 73
    }
527
}
528