Failed Conditions
Pull Request — master (#6709)
by Sergey
15:18
created

AbstractHydrator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
ccs 5
cts 5
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
crap 1
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
28
/**
29
 * Base class for all hydrators. A hydrator is a class that provides some form
30
 * of transformation of an SQL result set into another structure.
31
 *
32
 * @since  2.0
33
 * @author Konsta Vesterinen <[email protected]>
34
 * @author Roman Borschel <[email protected]>
35
 * @author Guilherme Blanco <[email protected]>
36
 */
37
abstract class AbstractHydrator
38
{
39
    /**
40
     * The ResultSetMapping.
41
     *
42
     * @var \Doctrine\ORM\Query\ResultSetMapping
43
     */
44
    protected $_rsm;
45
46
    /**
47
     * The EntityManager instance.
48
     *
49
     * @var EntityManagerInterface
50
     */
51
    protected $_em;
52
53
    /**
54
     * The dbms Platform instance.
55
     *
56
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
57
     */
58
    protected $_platform;
59
60
    /**
61
     * The UnitOfWork of the associated EntityManager.
62
     *
63
     * @var \Doctrine\ORM\UnitOfWork
64
     */
65
    protected $_uow;
66
67
    /**
68
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
69
     *
70
     * @var array
71
     */
72
    protected $_metadataCache = [];
73
74
    /**
75
     * The cache used during row-by-row hydration.
76
     *
77
     * @var array
78
     */
79
    protected $_cache = [];
80
81
    /**
82
     * The statement that provides the data to hydrate.
83
     *
84
     * @var \Doctrine\DBAL\Driver\Statement
85
     */
86
    protected $_stmt;
87
88
    /**
89
     * The query hints.
90
     *
91
     * @var array
92
     */
93
    protected $_hints;
94
95
    /**
96
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
97
     *
98
     * @param EntityManagerInterface $em The EntityManager to use.
99
     */
100 1021
    public function __construct(EntityManagerInterface $em)
101
    {
102 1021
        $this->_em       = $em;
103 1021
        $this->_platform = $em->getConnection()->getDatabasePlatform();
104 1021
        $this->_uow      = $em->getUnitOfWork();
105 1021
    }
106
107
    /**
108
     * Initiates a row-by-row hydration.
109
     *
110
     * @param object $stmt
111
     * @param object $resultSetMapping
112
     * @param array  $hints
113
     *
114
     * @return IterableResult
115
     */
116 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
117
    {
118 12
        $this->_stmt  = $stmt;
119 12
        $this->_rsm   = $resultSetMapping;
120 12
        $this->_hints = $hints;
121
122 12
        $evm = $this->_em->getEventManager();
123
124 12
        $evm->addEventListener([Events::onClear], $this);
125
126 12
        $this->prepare();
127
128 12
        return new IterableResult($this);
129
    }
130
131
    /**
132
     * Hydrates all rows returned by the passed statement instance at once.
133
     *
134
     * @param object $stmt
135
     * @param object $resultSetMapping
136
     * @param array  $hints
137
     *
138
     * @return array
139
     */
140 1009
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
141
    {
142 1009
        $this->_stmt  = $stmt;
143 1009
        $this->_rsm   = $resultSetMapping;
144 1009
        $this->_hints = $hints;
145
146 1009
        $this->_em->getEventManager()->addEventListener([Events::onClear], $this);
147
148 1009
        $this->prepare();
149
150 1008
        $result = $this->hydrateAllData();
151
152 998
        $this->cleanup();
153
154 998
        return $result;
155
    }
156
157
    /**
158
     * Hydrates a single row returned by the current statement instance during
159
     * row-by-row hydration with {@link iterate()}.
160
     *
161
     * @return mixed
162
     */
163 11
    public function hydrateRow()
164
    {
165 11
        $row = $this->_stmt->fetch(PDO::FETCH_ASSOC);
166
167 11
        if ( ! $row) {
168 8
            $this->cleanup();
169
170 8
            return false;
171
        }
172
173 10
        $result = [];
174
175 10
        $this->hydrateRowData($row, $result);
176
177 10
        return $result;
178
    }
179
180
    /**
181
     * When executed in a hydrate() loop we have to clear internal state to
182
     * decrease memory consumption.
183
     *
184
     * @param mixed $eventArgs
185
     *
186
     * @return void
187
     */
188 8
    public function onClear($eventArgs)
189
    {
190 8
    }
191
192
    /**
193
     * Executes one-time preparation tasks, once each time hydration is started
194
     * through {@link hydrateAll} or {@link iterate()}.
195
     *
196
     * @return void
197
     */
198 106
    protected function prepare()
199
    {
200 106
    }
201
202
    /**
203
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
204
     * through {@link hydrateAll} or {@link iterate()}.
205
     *
206
     * @return void
207
     */
208 1006
    protected function cleanup()
209
    {
210 1006
        $this->_stmt->closeCursor();
211
212 1006
        $this->_stmt          = null;
213 1006
        $this->_rsm           = null;
214 1006
        $this->_cache         = [];
215 1006
        $this->_metadataCache = [];
216
217
        $this
218 1006
            ->_em
219 1006
            ->getEventManager()
220 1006
            ->removeEventListener([Events::onClear], $this);
221 1006
    }
222
223
    /**
224
     * Hydrates a single row from the current statement instance.
225
     *
226
     * Template method.
227
     *
228
     * @param array $data   The row data.
229
     * @param array $result The result to fill.
230
     *
231
     * @return void
232
     *
233
     * @throws HydrationException
234
     */
235
    protected function hydrateRowData(array $data, array &$result)
236
    {
237
        throw new HydrationException("hydrateRowData() not implemented by this hydrator.");
238
    }
239
240
    /**
241
     * Hydrates all rows from the current statement instance at once.
242
     *
243
     * @return array
244
     */
245
    abstract protected function hydrateAllData();
246
247
    /**
248
     * Processes a row of the result set.
249
     *
250
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
251
     * Puts the elements of a result row into a new array, grouped by the dql alias
252
     * they belong to. The column names in the result set are mapped to their
253
     * field names during this procedure as well as any necessary conversions on
254
     * the values applied. Scalar values are kept in a specific key 'scalars'.
255
     *
256
     * @param array  $data               SQL Result Row.
257
     * @param array &$id                 Dql-Alias => ID-Hash.
258
     * @param array &$nonemptyComponents Does this DQL-Alias has at least one non NULL value?
259
     *
260
     * @return array  An array with all the fields (name => value) of the data row,
261
     *                grouped by their component alias.
262
     */
263 710
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
264
    {
265 710
        $rowData = ['data' => []];
266
267 710
        foreach ($data as $key => $value) {
268 710
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
269 8
                continue;
270
            }
271
272 710
            $fieldName = $cacheKeyInfo['fieldName'];
273
274
            switch (true) {
275 710
                case (isset($cacheKeyInfo['isNewObjectParameter'])):
276 22
                    $argIndex = $cacheKeyInfo['argIndex'];
277 22
                    $objIndex = $cacheKeyInfo['objIndex'];
278 22
                    $type     = $cacheKeyInfo['type'];
279 22
                    $value    = $type->convertToPHPValue($value, $this->_platform);
280
281 22
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
282 22
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
283
284 22
                    break;
285
286 695
                case (isset($cacheKeyInfo['isScalar'])):
287 102
                    $type  = $cacheKeyInfo['type'];
288 102
                    $value = $type->convertToPHPValue($value, $this->_platform);
289
290 102
                    $rowData['scalars'][$fieldName] = $value;
291 102
                    break;
292
293
                //case (isset($cacheKeyInfo['isMetaColumn'])):
0 ignored issues
show
Unused Code Comprehensibility introduced by
92% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
294
                default:
295 667
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
296 667
                    $type     = $cacheKeyInfo['type'];
297
298
                    // If there are field name collisions in the child class, then we need
299
                    // to only hydrate if we are looking at the correct discriminator value
300
                    if(
301 667
                        isset($cacheKeyInfo['discriminatorColumn']) && 
302 667
                        isset($data[$cacheKeyInfo['discriminatorColumn']]) &&
303
                        // Note: loose comparison required. See https://github.com/doctrine/doctrine2/pull/6304#issuecomment-323294442
304 667
                        $data[$cacheKeyInfo['discriminatorColumn']] != $cacheKeyInfo['discriminatorValue']
305
                    ) {
306 26
                        break;
307
                    }
308
309
                    // in an inheritance hierarchy the same field could be defined several times.
310
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
311
                    // Per definition it cannot be that a field is defined several times and has several values.
312 667
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
313
                        break;
314
                    }
315
316 667
                    $rowData['data'][$dqlAlias][$fieldName] = $type
317 667
                        ? $type->convertToPHPValue($value, $this->_platform)
318 1
                        : $value;
319
320 667
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
321 667
                        $id[$dqlAlias] .= '|' . $value;
322 667
                        $nonemptyComponents[$dqlAlias] = true;
323
                    }
324 710
                    break;
325
            }
326
        }
327
328 710
        foreach ($this->_rsm->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) {
329 1
            $newObject = $rowData['newObjects'][$objIndex];
330 1
            unset($rowData['newObjects'][$objIndex]);
331
332 1
            $class  = $newObject['class'];
333 1
            $args   = $newObject['args'];
334 1
            $obj    = $class->newInstanceArgs($args);
335
336 1
            $rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
0 ignored issues
show
Bug introduced by
The variable $ownerIndex does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $argIndex does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
337
        }
338
339
340 710
        if (isset($rowData['newObjects'])) {
341 22
            foreach ($rowData['newObjects'] as $objIndex => $newObject) {
342 22
                $class  = $newObject['class'];
343 22
                $args   = $newObject['args'];
344 22
                ksort($args);
345 22
                $obj    = $class->newInstanceArgs($args);
346
347 22
                $rowData['newObjects'][$objIndex]['obj'] = $obj;
348
            }
349
        }
350
351
352 710
        return $rowData;
353
    }
354
355
    /**
356
     * Processes a row of the result set.
357
     *
358
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
359
     * simply converts column names to field names and properly converts the
360
     * values according to their types. The resulting row has the same number
361
     * of elements as before.
362
     *
363
     * @param array $data
364
     *
365
     * @return array The processed row.
366
     */
367 98
    protected function gatherScalarRowData(&$data)
368
    {
369 98
        $rowData = [];
370
371 98
        foreach ($data as $key => $value) {
372 98
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
373 1
                continue;
374
            }
375
376 98
            $fieldName = $cacheKeyInfo['fieldName'];
377
378
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
379
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
380 98
            if ( ! isset($cacheKeyInfo['isScalar'])) {
381 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
382 49
                $type      = $cacheKeyInfo['type'];
383 49
                $fieldName = $dqlAlias . '_' . $fieldName;
384 49
                $value     = $type
385 49
                    ? $type->convertToPHPValue($value, $this->_platform)
386 49
                    : $value;
387
            }
388
389 98
            $rowData[$fieldName] = $value;
390
        }
391
392 98
        return $rowData;
393
    }
394
395
    /**
396
     * Retrieve column information from ResultSetMapping.
397
     *
398
     * @param string $key Column name
399
     *
400
     * @return array|null
401
     */
402 961
    protected function hydrateColumnInfo($key)
403
    {
404 961
        if (isset($this->_cache[$key])) {
405 437
            return $this->_cache[$key];
406
        }
407
408
        switch (true) {
409
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
410 961
            case (isset($this->_rsm->fieldMappings[$key])):
411 895
                $classMetadata = $this->getClassMetadata($this->_rsm->declaringClasses[$key]);
412 895
                $fieldName     = $this->_rsm->fieldMappings[$key];
413 895
                $fieldMapping  = $classMetadata->fieldMappings[$fieldName];
414 895
                $ownerMap      = $this->_rsm->columnOwnerMap[$key];
415
                $columnInfo    = [
416 895
                    'isIdentifier' => \in_array($fieldName, $classMetadata->identifier, true),
417 895
                    'fieldName'    => $fieldName,
418 895
                    'type'         => Type::getType($fieldMapping['type']),
419 895
                    'dqlAlias'     => $ownerMap,
420
                ];
421
422
                // the current discriminator value must be saved in order to disambiguate fields hydration,
423
                // should there be field name collisions
424 895
                if ($classMetadata->parentClasses && isset($this->_rsm->discriminatorColumns[$ownerMap])) {
425 105
                    return $this->_cache[$key] = \array_merge(
426 105
                        $columnInfo,
427
                        [
428 105
                            'discriminatorColumn' => $this->_rsm->discriminatorColumns[$ownerMap],
429 105
                            'discriminatorValue'  => $classMetadata->discriminatorValue
430
                        ]
431
                    );
432
                }
433
434 866
                return $this->_cache[$key] = $columnInfo;
435
436 758
            case (isset($this->_rsm->newObjectMappings[$key])):
437
                // WARNING: A NEW object is also a scalar, so it must be declared before!
438 22
                $mapping = $this->_rsm->newObjectMappings[$key];
439
440 22
                return $this->_cache[$key] = [
441 22
                    'isScalar'             => true,
442
                    'isNewObjectParameter' => true,
443 22
                    'fieldName'            => $this->_rsm->scalarMappings[$key],
444 22
                    'type'                 => Type::getType($this->_rsm->typeMappings[$key]),
445 22
                    'argIndex'             => $mapping['argIndex'],
446 22
                    'objIndex'             => $mapping['objIndex'],
447 22
                    'class'                => new \ReflectionClass($mapping['className']),
448
                ];
449
450 743
            case (isset($this->_rsm->scalarMappings[$key])):
451 153
                return $this->_cache[$key] = [
452 153
                    'isScalar'  => true,
453 153
                    'fieldName' => $this->_rsm->scalarMappings[$key],
454 153
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
455
                ];
456
457 638
            case (isset($this->_rsm->metaMappings[$key])):
458
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
459 633
                $fieldName = $this->_rsm->metaMappings[$key];
460 633
                $dqlAlias  = $this->_rsm->columnOwnerMap[$key];
461 633
                $type      = isset($this->_rsm->typeMappings[$key])
462 633
                    ? Type::getType($this->_rsm->typeMappings[$key])
463 633
                    : null;
464
465
                // Cache metadata fetch
466 633
                $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]);
467
468 633
                return $this->_cache[$key] = [
469 633
                    'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
470
                    'isMetaColumn' => true,
471 633
                    'fieldName'    => $fieldName,
472 633
                    'type'         => $type,
473 633
                    'dqlAlias'     => $dqlAlias,
474
                ];
475
        }
476
477
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
478
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
479 10
        return null;
480
    }
481
482
    /**
483
     * Retrieve ClassMetadata associated to entity class name.
484
     *
485
     * @param string $className
486
     *
487
     * @return \Doctrine\ORM\Mapping\ClassMetadata
488
     */
489 920
    protected function getClassMetadata($className)
490
    {
491 920
        if ( ! isset($this->_metadataCache[$className])) {
492 920
            $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
493
        }
494
495 920
        return $this->_metadataCache[$className];
496
    }
497
498
    /**
499
     * Register entity as managed in UnitOfWork.
500
     *
501
     * @param ClassMetadata $class
502
     * @param object        $entity
503
     * @param array         $data
504
     *
505
     * @return void
506
     *
507
     * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
508
     */
509 72
    protected function registerManaged(ClassMetadata $class, $entity, array $data)
510
    {
511 72
        if ($class->isIdentifierComposite) {
512 5
            $id = [];
513
514 5
            foreach ($class->identifier as $fieldName) {
515 5
                $id[$fieldName] = isset($class->associationMappings[$fieldName])
516 3
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
517 5
                    : $data[$fieldName];
518
            }
519
        } else {
520 67
            $fieldName = $class->identifier[0];
521
            $id        = [
522 67
                $fieldName => isset($class->associationMappings[$fieldName])
523
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
524 67
                    : $data[$fieldName]
525
            ];
526
        }
527
528 72
        $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data);
529 72
    }
530
}
531