Failed Conditions
Pull Request — master (#6649)
by Marco
140:45 queued 75:42
created

AbstractHydrator::getClassMetadata()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 2
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\Internal\Hydration\Cache\LazyPropertyMap;
26
use Doctrine\ORM\Mapping\ClassMetadata;
27
use PDO;
28
29
/**
30
 * Base class for all hydrators. A hydrator is a class that provides some form
31
 * of transformation of an SQL result set into another structure.
32
 *
33
 * @since  2.0
34
 * @author Konsta Vesterinen <[email protected]>
35
 * @author Roman Borschel <[email protected]>
36
 * @author Guilherme Blanco <[email protected]>
37
 */
38
abstract class AbstractHydrator
39
{
40
    /**
41
     * The ResultSetMapping.
42
     *
43
     * @var \Doctrine\ORM\Query\ResultSetMapping
44
     */
45
    protected $_rsm;
46
47
    /**
48
     * The EntityManager instance.
49
     *
50
     * @var EntityManagerInterface
51
     */
52
    protected $_em;
53
54
    /**
55
     * The dbms Platform instance.
56
     *
57
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
58
     */
59
    protected $_platform;
60
61
    /**
62
     * The UnitOfWork of the associated EntityManager.
63
     *
64
     * @var \Doctrine\ORM\UnitOfWork
65
     */
66
    protected $_uow;
67
68
    /**
69
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
70
     *
71
     * @var ClassMetadata[]|LazyPropertyMap indexed by class name
72
     */
73
    protected $_metadataCache;
74
75
    /**
76
     * The cache used during row-by-row hydration.
77
     *
78
     * @var array
79
     */
80
    protected $_cache = [];
81
82
    /**
83
     * The statement that provides the data to hydrate.
84
     *
85
     * @var \Doctrine\DBAL\Driver\Statement
86
     */
87
    protected $_stmt;
88
89
    /**
90
     * The query hints.
91
     *
92
     * @var array
93
     */
94
    protected $_hints;
95
96
    /**
97
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
98
     *
99
     * @param EntityManagerInterface $em The EntityManager to use.
100 1020
     */
101
    public function __construct(EntityManagerInterface $em)
102 1020
    {
103 1020
        $this->_em            = $em;
104 1020
        $this->_platform      = $em->getConnection()->getDatabasePlatform();
105 1020
        $this->_uow           = $em->getUnitOfWork();
106
        $this->_metadataCache = new LazyPropertyMap([$em, 'getClassMetadata']);
107
    }
108
109
    /**
110
     * Initiates a row-by-row hydration.
111
     *
112
     * @param object $stmt
113
     * @param object $resultSetMapping
114
     * @param array  $hints
115
     *
116 12
     * @return IterableResult
117
     */
118 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
119 12
    {
120 12
        $this->_stmt  = $stmt;
121
        $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
        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 1008
     * @return array
141
     */
142 1008
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
143 1008
    {
144 1008
        $this->_stmt  = $stmt;
145
        $this->_rsm   = $resultSetMapping;
146 1008
        $this->_hints = $hints;
147
148 1008
        $this->_em->getEventManager()->addEventListener([Events::onClear], $this);
149
150 1007
        $this->prepare();
151
152 997
        $result = $this->hydrateAllData();
153
154 997
        $this->cleanup();
155
156
        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 11
     * @return mixed
164
     */
165 11 View Code Duplication
    public function hydrateRow()
166
    {
167 11
        $row = $this->_stmt->fetch(PDO::FETCH_ASSOC);
168 8
169
        if ( ! $row) {
170 8
            $this->cleanup();
171
172
            return false;
173 10
        }
174
175 10
        $result = [];
176
177 10
        $this->hydrateRowData($row, $result);
178
179
        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 8
     * @return void
189
     */
190 8
    public function onClear($eventArgs)
191
    {
192
    }
193
194
    /**
195
     * Executes one-time preparation tasks, once each time hydration is started
196
     * through {@link hydrateAll} or {@link iterate()}.
197
     *
198 106
     * @return void
199
     */
200 106
    protected function prepare()
201
    {
202
    }
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 1005
     * @return void
209
     */
210 1005
    protected function cleanup()
211
    {
212 1005
        $this->_stmt->closeCursor();
213 1005
214 1005
        $this->_stmt          = null;
215 1005
        $this->_rsm           = null;
216
        $this->_cache         = [];
217
        $this->_metadataCache = new LazyPropertyMap([$this->_em, 'getClassMetadata']);
218 1005
219 1005
        $this
220 1005
            ->_em
221 1005
            ->getEventManager()
222
            ->removeEventListener([Events::onClear], $this);
223
    }
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)
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 709
     *                grouped by their component alias.
264
     */
265 709
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
266
    {
267 709
        $rowData = ['data' => []];
268 709
269 8
        foreach ($data as $key => $value) {
270
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
271
                continue;
272 709
            }
273
274
            $fieldName = $cacheKeyInfo['fieldName'];
275 709
276 21
            switch (true) {
277 21
                case (isset($cacheKeyInfo['isNewObjectParameter'])):
278 21
                    $argIndex = $cacheKeyInfo['argIndex'];
279 21
                    $objIndex = $cacheKeyInfo['objIndex'];
280
                    $type     = $cacheKeyInfo['type'];
281 21
                    $value    = $type->convertToPHPValue($value, $this->_platform);
282 21
283 21
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
284
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
285 694
                    break;
286 101
287 101
                case (isset($cacheKeyInfo['isScalar'])):
288
                    $type  = $cacheKeyInfo['type'];
289 101
                    $value = $type->convertToPHPValue($value, $this->_platform);
290 101
291
                    $rowData['scalars'][$fieldName] = $value;
292
                    break;
293
294 667
                //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...
295 667
                default:
296
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
297
                    $type     = $cacheKeyInfo['type'];
298
299
                    // If there are field name collisions in the child class, then we need
300 667
                    // to only hydrate if we are looking at the correct discriminator value
301 667
                    if(
302
                        isset($cacheKeyInfo['discriminatorColumn']) && 
303 667
                        isset($data[$cacheKeyInfo['discriminatorColumn']]) &&
304
                        // Note: loose comparison required. See https://github.com/doctrine/doctrine2/pull/6304#issuecomment-323294442
305 26
                        $data[$cacheKeyInfo['discriminatorColumn']] != $cacheKeyInfo['discriminatorValue']
306
                    ) {
307
                        break;
308
                    }
309
310
                    // in an inheritance hierarchy the same field could be defined several times.
311 667
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
312
                    // Per definition it cannot be that a field is defined several times and has several values.
313
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
314
                        break;
315 667
                    }
316 667
317 1
                    $rowData['data'][$dqlAlias][$fieldName] = $type
318
                        ? $type->convertToPHPValue($value, $this->_platform)
319 667
                        : $value;
320 667
321 667
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
322
                        $id[$dqlAlias] .= '|' . $value;
323 709
                        $nonemptyComponents[$dqlAlias] = true;
324
                    }
325
                    break;
326
            }
327 709
        }
328
329
        return $rowData;
330
    }
331
332
    /**
333
     * Processes a row of the result set.
334
     *
335
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
336
     * simply converts column names to field names and properly converts the
337
     * values according to their types. The resulting row has the same number
338
     * of elements as before.
339
     *
340
     * @param array $data
341
     *
342 98
     * @return array The processed row.
343
     */
344 98
    protected function gatherScalarRowData(&$data)
345
    {
346 98
        $rowData = [];
347 98
348 1
        foreach ($data as $key => $value) {
349
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
350
                continue;
351 98
            }
352
353
            $fieldName = $cacheKeyInfo['fieldName'];
354
355 98
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
356 49
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
357 49
            if ( ! isset($cacheKeyInfo['isScalar'])) {
358 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
359 49
                $type      = $cacheKeyInfo['type'];
360 49
                $fieldName = $dqlAlias . '_' . $fieldName;
361 49
                $value     = $type
362
                    ? $type->convertToPHPValue($value, $this->_platform)
363
                    : $value;
364 98
            }
365
366
            $rowData[$fieldName] = $value;
367 98
        }
368
369
        return $rowData;
370
    }
371
372
    /**
373
     * Retrieve column information from ResultSetMapping.
374
     *
375
     * @param string $key Column name
376
     *
377 960
     * @return array|null
378
     */
379 960
    protected function hydrateColumnInfo($key)
380 436
    {
381
        if (isset($this->_cache[$key])) {
382
            return $this->_cache[$key];
383
        }
384
385 960
        switch (true) {
386 895
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
387 895
            case (isset($this->_rsm->fieldMappings[$key])):
388 895
                /* @var $classMetadata ClassMetadata */
389 895
                $classMetadata = $this->_metadataCache->{$this->_rsm->declaringClasses[$key]};
390
                $fieldName     = $this->_rsm->fieldMappings[$key];
391 895
                $fieldMapping  = $classMetadata->fieldMappings[$fieldName];
392 895
                $ownerMap      = $this->_rsm->columnOwnerMap[$key];
393 895
                $columnInfo    = [
394 895
                    'isIdentifier' => \in_array($fieldName, $classMetadata->identifier, true),
395
                    'fieldName'    => $fieldName,
396
                    'type'         => Type::getType($fieldMapping['type']),
397
                    'dqlAlias'     => $ownerMap,
398
                ];
399 895
400 105
                // the current discriminator value must be saved in order to disambiguate fields hydration,
401 105
                // should there be field name collisions
402
                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...
403 105
                    return $this->_cache[$key] = \array_merge(
404 105
                        $columnInfo,
405
                        [
406
                            'discriminatorColumn' => $this->_rsm->discriminatorColumns[$ownerMap],
407
                            'discriminatorValue'  => $classMetadata->discriminatorValue
408
                        ]
409 866
                    );
410
                }
411 757
412
                return $this->_cache[$key] = $columnInfo;
413 21
414
            case (isset($this->_rsm->newObjectMappings[$key])):
415 21
                // WARNING: A NEW object is also a scalar, so it must be declared before!
416 21
                $mapping = $this->_rsm->newObjectMappings[$key];
417
418 21
                return $this->_cache[$key] = [
419 21
                    'isScalar'             => true,
420 21
                    'isNewObjectParameter' => true,
421 21
                    'fieldName'            => $this->_rsm->scalarMappings[$key],
422 21
                    'type'                 => Type::getType($this->_rsm->typeMappings[$key]),
423
                    'argIndex'             => $mapping['argIndex'],
424
                    'objIndex'             => $mapping['objIndex'],
425 742
                    'class'                => new \ReflectionClass($mapping['className']),
426 152
                ];
427 152
428 152
            case (isset($this->_rsm->scalarMappings[$key])):
429 152
                return $this->_cache[$key] = [
430
                    'isScalar'  => true,
431
                    'fieldName' => $this->_rsm->scalarMappings[$key],
432 638
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
433
                ];
434 633
435 633
            case (isset($this->_rsm->metaMappings[$key])):
436 633
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
437 633
                $fieldName = $this->_rsm->metaMappings[$key];
438 633
                $dqlAlias  = $this->_rsm->columnOwnerMap[$key];
439
                $type      = isset($this->_rsm->typeMappings[$key])
440
                    ? Type::getType($this->_rsm->typeMappings[$key])
441 633
                    : null;
442
443 633
                // Cache metadata fetch
444 633
                $this->_metadataCache->{$this->_rsm->aliasMap[$dqlAlias]};
445
446 633
                return $this->_cache[$key] = [
447 633
                    'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
448 633
                    'isMetaColumn' => true,
449
                    'fieldName'    => $fieldName,
450
                    'type'         => $type,
451
                    'dqlAlias'     => $dqlAlias,
452
                ];
453
        }
454 10
455
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
456
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
457
        return null;
458
    }
459
460
    /**
461
     * Register entity as managed in UnitOfWork.
462
     *
463
     * @param ClassMetadata $class
464 920
     * @param object        $entity
465
     * @param array         $data
466 920
     *
467 920
     * @return void
468
     *
469
     * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
470 920
     */
471
    protected function registerManaged(ClassMetadata $class, $entity, array $data)
472
    {
473
        if ($class->isIdentifierComposite) {
474
            $id = [];
475
476
            foreach ($class->identifier as $fieldName) {
477
                $id[$fieldName] = isset($class->associationMappings[$fieldName])
478
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
479
                    : $data[$fieldName];
480
            }
481
        } else {
482
            $fieldName = $class->identifier[0];
483
            $id        = [
484 72
                $fieldName => isset($class->associationMappings[$fieldName])
485
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
486 72
                    : $data[$fieldName]
487 5
            ];
488
        }
489 5
490 5
        $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data);
491 3
    }
492
}
493