Passed
Push — 2.7 ( f576e6...8420d2 )
by Luís
08:14 queued 11s
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 1070
    public function __construct(EntityManagerInterface $em)
104
    {
105 1070
        $this->_em       = $em;
106 1070
        $this->_platform = $em->getConnection()->getDatabasePlatform();
107 1070
        $this->_uow      = $em->getUnitOfWork();
108 1070
    }
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 1058
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
144
    {
145 1058
        $this->_stmt  = $stmt;
146 1058
        $this->_rsm   = $resultSetMapping;
147 1058
        $this->_hints = $hints;
148
149 1058
        $this->_em->getEventManager()->addEventListener([Events::onClear], $this);
150
151 1058
        $this->prepare();
152
153 1057
        $result = $this->hydrateAllData();
154
155 1047
        $this->cleanup();
156
157 1047
        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 82
    protected function prepare()
202
    {
203 82
    }
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 1055
    protected function cleanup()
212
    {
213 1055
        $this->_stmt->closeCursor();
214
215 1055
        $this->_stmt          = null;
216 1055
        $this->_rsm           = null;
217 1055
        $this->_cache         = [];
218 1055
        $this->_metadataCache = [];
219
220
        $this
221 1055
            ->_em
222 1055
            ->getEventManager()
223 1055
            ->removeEventListener([Events::onClear], $this);
224 1055
    }
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 738
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
267
    {
268 738
        $rowData = ['data' => []];
269
270 738
        foreach ($data as $key => $value) {
271 738
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
272 8
                continue;
273
            }
274
275 738
            $fieldName = $cacheKeyInfo['fieldName'];
276
277
            switch (true) {
278 738
                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 723
                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 683
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
298 683
                    $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 683
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
303 683
                        && ! 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 683
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
312
                        break;
313
                    }
314
315 683
                    $rowData['data'][$dqlAlias][$fieldName] = $type
316 683
                        ? $type->convertToPHPValue($value, $this->_platform)
317 1
                        : $value;
318
319 683
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
320 683
                        $id[$dqlAlias] .= '|' . $value;
321 683
                        $nonemptyComponents[$dqlAlias] = true;
322
                    }
323 738
                    break;
324
            }
325
        }
326
327 738
        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 74
    protected function gatherScalarRowData(&$data)
343
    {
344 74
        $rowData = [];
345
346 74
        foreach ($data as $key => $value) {
347 74
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
348 1
                continue;
349
            }
350
351 74
            $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 74
            if (! isset($cacheKeyInfo['isScalar'])) {
356 44
                $type  = $cacheKeyInfo['type'];
357 44
                $value = $type ? $type->convertToPHPValue($value, $this->_platform) : $value;
358
359 44
                $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName;
360
            }
361
362 74
            $rowData[$fieldName] = $value;
363
        }
364
365 74
        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 1001
    protected function hydrateColumnInfo($key)
376
    {
377 1001
        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 1001
            case (isset($this->_rsm->fieldMappings[$key])):
384 923
                $classMetadata = $this->getClassMetadata($this->_rsm->declaringClasses[$key]);
385 923
                $fieldName     = $this->_rsm->fieldMappings[$key];
386 923
                $fieldMapping  = $classMetadata->fieldMappings[$fieldName];
387 923
                $ownerMap      = $this->_rsm->columnOwnerMap[$key];
388
                $columnInfo    = [
389 923
                    'isIdentifier' => \in_array($fieldName, $classMetadata->identifier, true),
390 923
                    'fieldName'    => $fieldName,
391 923
                    'type'         => Type::getType($fieldMapping['type']),
392 923
                    '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 923
                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 894
                return $this->_cache[$key] = $columnInfo;
409
410 787
            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 772
            case isset($this->_rsm->scalarMappings[$key], $this->_hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]):
425 12
                return $this->_cache[$key] = [
426 12
                    'fieldName' => $this->_rsm->scalarMappings[$key],
427 12
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
428 12
                    'dqlAlias'  => '',
429
                ];
430 772
            case (isset($this->_rsm->scalarMappings[$key])):
431 147
                return $this->_cache[$key] = [
432 147
                    'isScalar'  => true,
433 147
                    'fieldName' => $this->_rsm->scalarMappings[$key],
434 147
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
435
                ];
436
437 655
            case (isset($this->_rsm->metaMappings[$key])):
438
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
439 650
                $fieldName = $this->_rsm->metaMappings[$key];
440 650
                $dqlAlias  = $this->_rsm->columnOwnerMap[$key];
441 650
                $type      = isset($this->_rsm->typeMappings[$key])
442 650
                    ? Type::getType($this->_rsm->typeMappings[$key])
443 650
                    : null;
444
445
                // Cache metadata fetch
446 650
                $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]);
447
448 650
                return $this->_cache[$key] = [
449 650
                    'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
450
                    'isMetaColumn' => true,
451 650
                    'fieldName'    => $fieldName,
452 650
                    'type'         => $type,
453 650
                    '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 949
    protected function getClassMetadata($className)
487
    {
488 949
        if ( ! isset($this->_metadataCache[$className])) {
489 949
            $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
490
        }
491
492 949
        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