Failed Conditions
CANCELLED  
Pull Request — 2.6 (#7180)
by Ben
07:44
created

AbstractHydrator::hydrateColumnInfo()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 79
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 9

Importance

Changes 0
Metric Value
cc 9
eloc 49
nc 8
nop 1
dl 0
loc 79
rs 7.5571
c 0
b 0
f 0
ccs 46
cts 46
cp 1
crap 9

How to fix   Long Method   

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 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 1022
    public function __construct(EntityManagerInterface $em)
103
    {
104 1022
        $this->_em       = $em;
105 1022
        $this->_platform = $em->getConnection()->getDatabasePlatform();
106 1022
        $this->_uow      = $em->getUnitOfWork();
107 1022
    }
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 1010
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
143
    {
144 1010
        $this->_stmt  = $stmt;
145 1010
        $this->_rsm   = $resultSetMapping;
146 1010
        $this->_hints = $hints;
147
148 1010
        $this->_em->getEventManager()->addEventListener([Events::onClear], $this);
149
150 1010
        $this->prepare();
151
152 1009
        $result = $this->hydrateAllData();
153
154 999
        $this->cleanup();
155
156 999
        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 1007
    protected function cleanup()
211
    {
212 1007
        $this->_stmt->closeCursor();
213
214 1007
        $this->_stmt          = null;
215 1007
        $this->_rsm           = null;
216 1007
        $this->_cache         = [];
217 1007
        $this->_metadataCache = [];
218
219
        $this
220 1007
            ->_em
221 1007
            ->getEventManager()
222 1007
            ->removeEventListener([Events::onClear], $this);
223 1007
    }
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 712
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
266
    {
267 712
        $rowData = ['data' => []];
268
269 712
        foreach ($data as $key => $value) {
270 712
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
271 8
                continue;
272
            }
273
274 712
            $fieldName = $cacheKeyInfo['fieldName'];
275
276
            switch (true) {
277 712
                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 697
                case (isset($cacheKeyInfo['isScalar'])):
288 114
                    $type  = $cacheKeyInfo['type'];
289 114
                    $value = $type->convertToPHPValue($value, $this->_platform);
290
291 114
                    $rowData['scalars'][$fieldName] = $value;
292 114
                    break;
293
294
                //case (isset($cacheKeyInfo['isMetaColumn'])):
295
                default:
296 657
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
297 657
                    $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 657
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
302 657
                        && ! 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 657
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
311
                        break;
312
                    }
313
314 657
                    $rowData['data'][$dqlAlias][$fieldName] = $type
315 657
                        ? $type->convertToPHPValue($value, $this->_platform)
316 1
                        : $value;
317
318 657
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
319 657
                        $id[$dqlAlias] .= '|' . $value;
320 657
                        $nonemptyComponents[$dqlAlias] = true;
321
                    }
322 712
                    break;
323
            }
324
        }
325
326 712
        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 958
    protected function hydrateColumnInfo($key)
377
    {
378 958
        if (isset($this->_cache[$key])) {
379 436
            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 958
            case (isset($this->_rsm->fieldMappings[$key])):
385 880
                $classMetadata = $this->getClassMetadata($this->_rsm->declaringClasses[$key]);
386 880
                $fieldName     = $this->_rsm->fieldMappings[$key];
387 880
                $fieldMapping  = $classMetadata->fieldMappings[$fieldName];
388 880
                $ownerMap      = $this->_rsm->columnOwnerMap[$key];
389
                $columnInfo    = [
390 880
                    'isIdentifier' => \in_array($fieldName, $classMetadata->identifier, true),
391 880
                    'fieldName'    => $fieldName,
392 880
                    'type'         => Type::getType($fieldMapping['type']),
393 880
                    '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 880
                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 851
                return $this->_cache[$key] = $columnInfo;
410
411 752
            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 737
            case (isset($this->_rsm->scalarMappings[$key])):
426 165
                return $this->_cache[$key] = [
427 165
                    'isScalar'  => true,
428 165
                    'fieldName' => $this->_rsm->scalarMappings[$key],
429 165
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
430
                ];
431
432 620
            case (isset($this->_rsm->metaMappings[$key])):
433
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
434 615
                $fieldName = $this->_rsm->metaMappings[$key];
435 615
                $dqlAlias  = $this->_rsm->columnOwnerMap[$key];
436 615
                $type      = isset($this->_rsm->typeMappings[$key])
437 615
                    ? Type::getType($this->_rsm->typeMappings[$key])
438 615
                    : null;
439
440
                // Cache metadata fetch
441 615
                $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]);
442
443 615
                return $this->_cache[$key] = [
444 615
                    'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
445
                    'isMetaColumn' => true,
446 615
                    'fieldName'    => $fieldName,
447 615
                    'type'         => $type,
448 615
                    '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 906
    protected function getClassMetadata($className)
482
    {
483 906
        if ( ! isset($this->_metadataCache[$className])) {
484 906
            $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
485
        }
486
487 906
        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 67
    protected function registerManaged(ClassMetadata $class, $entity, array $data)
502
    {
503 67
        if ($class->isIdentifierComposite) {
504 2
            $id = [];
505
506 2
            foreach ($class->identifier as $fieldName) {
507 2
                $id[$fieldName] = isset($class->associationMappings[$fieldName])
508
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
509 2
                    : $data[$fieldName];
510
            }
511
        } else {
512 65
            $fieldName = $class->identifier[0];
513
            $id        = [
514 65
                $fieldName => isset($class->associationMappings[$fieldName])
515
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
516 65
                    : $data[$fieldName]
517
            ];
518
        }
519
520 67
        $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data);
521 67
    }
522
}
523