Failed Conditions
Push — develop ( d1b453...981221 )
by Marco
05:38 queued 05:25
created

AbstractHydrator::hydrateAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 9

Duplication

Lines 16
Ratio 100 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 16
loc 16
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 9
nc 1
nop 3
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Internal\Hydration;
6
7
use Doctrine\ORM\EntityManagerInterface;
8
use Doctrine\ORM\Events;
9
use PDO;
10
11
/**
12
 * Base class for all hydrators. A hydrator is a class that provides some form
13
 * of transformation of an SQL result set into another structure.
14
 *
15
 * @since  2.0
16
 * @author Konsta Vesterinen <[email protected]>
17
 * @author Roman Borschel <[email protected]>
18
 * @author Guilherme Blanco <[email protected]>
19
 */
20
abstract class AbstractHydrator
21
{
22
    /**
23
     * The ResultSetMapping.
24
     *
25
     * @var \Doctrine\ORM\Query\ResultSetMapping
26
     */
27
    protected $rsm;
28
29
    /**
30
     * The EntityManager instance.
31
     *
32
     * @var EntityManagerInterface
33
     */
34
    protected $em;
35
36
    /**
37
     * The dbms Platform instance.
38
     *
39
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
40
     */
41
    protected $platform;
42
43
    /**
44
     * The UnitOfWork of the associated EntityManager.
45
     *
46
     * @var \Doctrine\ORM\UnitOfWork
47
     */
48
    protected $uow;
49
50
    /**
51
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
52
     *
53
     * @var array
54
     */
55
    protected $metadataCache = [];
56
57
    /**
58
     * The cache used during row-by-row hydration.
59
     *
60
     * @var array
61
     */
62
    protected $cache = [];
63
64
    /**
65
     * The statement that provides the data to hydrate.
66
     *
67
     * @var \Doctrine\DBAL\Driver\Statement
68
     */
69
    protected $stmt;
70
71
    /**
72
     * The query hints.
73
     *
74
     * @var array
75
     */
76
    protected $hints;
77
78
    /**
79
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
80
     *
81
     * @param EntityManagerInterface $em The EntityManager to use.
82
     */
83
    public function __construct(EntityManagerInterface $em)
84
    {
85
        $this->em       = $em;
86
        $this->platform = $em->getConnection()->getDatabasePlatform();
87
        $this->uow      = $em->getUnitOfWork();
88
    }
89
90
    /**
91
     * Initiates a row-by-row hydration.
92
     *
93
     * @param object $stmt
94
     * @param object $resultSetMapping
95
     * @param array  $hints
96
     *
97
     * @return IterableResult
98
     */
99 View Code Duplication
    public function iterate($stmt, $resultSetMapping, array $hints = [])
100 971
    {
101
        $this->stmt  = $stmt;
102 971
        $this->rsm   = $resultSetMapping;
103 971
        $this->hints = $hints;
104 971
105 971
        $evm = $this->em->getEventManager();
106
107
        $evm->addEventListener([Events::onClear], $this);
108
109
        $this->prepare();
110
111
        return new IterableResult($this);
112
    }
113
114
    /**
115
     * Hydrates all rows returned by the passed statement instance at once.
116 11
     *
117
     * @param object $stmt
118 11
     * @param object $resultSetMapping
119 11
     * @param array  $hints
120 11
     *
121
     * @return array
122 11
     */
123 View Code Duplication
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
124 11
    {
125
        $this->stmt  = $stmt;
126 11
        $this->rsm   = $resultSetMapping;
127
        $this->hints = $hints;
128 11
129
        $this->em->getEventManager()->addEventListener([Events::onClear], $this);
130
131
        $this->prepare();
132
133
        $result = $this->hydrateAllData();
134
135
        $this->cleanup();
136
137
        return $result;
138
    }
139
140 960
    /**
141
     * Hydrates a single row returned by the current statement instance during
142 960
     * row-by-row hydration with {@link iterate()}.
143 960
     *
144 960
     * @return mixed
145
     */
146 960 View Code Duplication
    public function hydrateRow()
147
    {
148 959
        $row = $this->stmt->fetch(PDO::FETCH_ASSOC);
149
150 949
        if (! $row) {
151
            $this->cleanup();
152 949
153
            return false;
154
        }
155
156
        $result = [];
157
158
        $this->hydrateRowData($row, $result);
159
160
        return $result;
161 10
    }
162
163 10
    /**
164
     * When executed in a hydrate() loop we have to clear internal state to
165 10
     * decrease memory consumption.
166 7
     *
167
     * @param mixed $eventArgs
168 7
     *
169
     * @return void
170
     */
171 10
    public function onClear($eventArgs)
172
    {
173 10
    }
174
175 10
    /**
176
     * Executes one-time preparation tasks, once each time hydration is started
177
     * through {@link hydrateAll} or {@link iterate()}.
178
     *
179
     * @return void
180
     */
181
    protected function prepare()
182
    {
183
    }
184
185
    /**
186 6
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
187
     * through {@link hydrateAll} or {@link iterate()}.
188 6
     *
189
     * @return void
190
     */
191
    protected function cleanup()
192
    {
193
        $this->stmt->closeCursor();
194
195
        $this->stmt          = null;
196 103
        $this->rsm           = null;
197
        $this->cache         = [];
198 103
        $this->metadataCache = [];
199
200
        $this->em
201
             ->getEventManager()
202
             ->removeEventListener([Events::onClear], $this);
203
    }
204
205
    /**
206 956
     * Hydrates a single row from the current statement instance.
207
     *
208 956
     * Template method.
209
     *
210 956
     * @param array $data   The row data.
211 956
     * @param array $result The result to fill.
212 956
     *
213 956
     * @return void
214 956
     *
215
     * @throws HydrationException
216
     */
217
    protected function hydrateRowData(array $data, array &$result)
218
    {
219
        throw new HydrationException("hydrateRowData() not implemented by this hydrator.");
220
    }
221
222
    /**
223
     * Hydrates all rows from the current statement instance at once.
224
     *
225
     * @return array
226
     */
227
    abstract protected function hydrateAllData();
228
229
    /**
230
     * Processes a row of the result set.
231
     *
232
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
233
     * Puts the elements of a result row into a new array, grouped by the dql alias
234
     * they belong to. The column names in the result set are mapped to their
235
     * field names during this procedure as well as any necessary conversions on
236
     * the values applied. Scalar values are kept in a specific key 'scalars'.
237
     *
238
     * @param array  $data               SQL Result Row.
239
     * @param array &$id                 Dql-Alias => ID-Hash.
240
     * @param array &$nonemptyComponents Does this DQL-Alias has at least one non NULL value?
241
     *
242
     * @return array  An array with all the fields (name => value) of the data row,
243
     *                grouped by their component alias.
244
     */
245
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
246
    {
247
        $rowData = ['data' => []];
248
249
        foreach ($data as $key => $value) {
250
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
251
                continue;
252
            }
253
254
            $fieldName = $cacheKeyInfo['fieldName'];
255
256 683
            switch (true) {
257
                case (isset($cacheKeyInfo['isNewObjectParameter'])):
258 683
                    $argIndex = $cacheKeyInfo['argIndex'];
259
                    $objIndex = $cacheKeyInfo['objIndex'];
260 683
                    $type     = $cacheKeyInfo['type'];
261 683
                    $value    = $type->convertToPHPValue($value, $this->platform);
262 8
263
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
264
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
265 683
                    break;
266
267
                case (isset($cacheKeyInfo['isScalar'])):
268 683
                    $type  = $cacheKeyInfo['type'];
269 21
                    $value = $type->convertToPHPValue($value, $this->platform);
270 21
271 21
                    $rowData['scalars'][$fieldName] = $value;
272 21
                    break;
273
274 21
                //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...
275 21
                default:
276 21
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
277
                    $type     = $cacheKeyInfo['type'];
278 668
279 98
                    // If there are field name collisions in the child class, then we need
280 98
                    // to only hydrate if we are looking at the correct discriminator value
281
                    if(
282 98
                        isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']]) &&
283 98
                        // Note: loose comparison required. See https://github.com/doctrine/doctrine2/pull/6304#issuecomment-323294442
284
                        $data[$cacheKeyInfo['discriminatorColumn']] != $cacheKeyInfo['discriminatorValue']
285
                    ) {
286
                        break;
287 642
                    }
288 642
289
                    // in an inheritance hierarchy the same field could be defined several times.
290
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
291
                    // Per definition it cannot be that a field is defined several times and has several values.
292
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
293 642
                        break;
294 3
                    }
295
296
                    $rowData['data'][$dqlAlias][$fieldName] = $type
297 642
                        ? $type->convertToPHPValue($value, $this->platform)
298 642
                        : $value;
299
300
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
301 642
                        $id[$dqlAlias] .= '|' . $value;
302 642
                        $nonemptyComponents[$dqlAlias] = true;
303 642
                    }
304
                    break;
305 683
            }
306
        }
307
308
        return $rowData;
309 683
    }
310
311
    /**
312
     * Processes a row of the result set.
313
     *
314
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
315
     * simply converts column names to field names and properly converts the
316
     * values according to their types. The resulting row has the same number
317
     * of elements as before.
318
     *
319
     * @param array $data
320
     *
321
     * @return array The processed row.
322
     */
323
    protected function gatherScalarRowData(&$data)
324 97
    {
325
        $rowData = [];
326 97
327
        foreach ($data as $key => $value) {
328 97
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
329 97
                continue;
330 1
            }
331
332
            $fieldName = $cacheKeyInfo['fieldName'];
333 97
334
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
335
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
336
            if ( ! isset($cacheKeyInfo['isScalar'])) {
337 97
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
338 49
                $type      = $cacheKeyInfo['type'];
339 49
                $fieldName = $dqlAlias . '_' . $fieldName;
340 49
                $value     = $type
341 49
                    ? $type->convertToPHPValue($value, $this->platform)
342 49
                    : $value;
343 49
            }
344
345
            $rowData[$fieldName] = $value;
346 97
        }
347
348
        return $rowData;
349 97
    }
350
351
    /**
352
     * Retrieve column information from ResultSetMapping.
353
     *
354
     * @param string $key Column name
355
     *
356
     * @return array|null
357
     */
358
    protected function hydrateColumnInfo($key)
359 924
    {
360
        if (isset($this->cache[$key])) {
361 924
            return $this->cache[$key];
362 419
        }
363
364
        switch (true) {
365
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
366
            case (isset($this->rsm->fieldMappings[$key])):
367 924
                $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
368 861
                $fieldName     = $this->rsm->fieldMappings[$key];
369 861
                $ownerMap      = $this->rsm->columnOwnerMap[$key];
370 861
                $property      = $classMetadata->getProperty($fieldName);
371
372 861
                $columnInfo = [
373 861
                    'isIdentifier' => $property->isPrimaryKey(),
374 861
                    'fieldName'    => $fieldName,
375 861
                    'type'         => $property->getType(),
376 861
                    'dqlAlias'     => $this->rsm->columnOwnerMap[$key],
377
                ];
378
379 732
                // the current discriminator value must be saved in order to disambiguate fields hydration,
380
                // should there be field name collisions
381 21
                if ($classMetadata->getParent() && isset($this->rsm->discriminatorColumns[$ownerMap])) {
382
                    return $this->cache[$key] = \array_merge(
383 21
                        $columnInfo,
384 21
                        [
385
                            'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
386 21
                            'discriminatorValue'  => $classMetadata->discriminatorValue
387 21
                        ]
388 21
                    );
389 21
                }
390 21
391
                return $this->cache[$key] = $columnInfo;
392
393 717
            case (isset($this->rsm->newObjectMappings[$key])):
394 148
                // WARNING: A NEW object is also a scalar, so it must be declared before!
395 148
                $mapping = $this->rsm->newObjectMappings[$key];
396 148
397 148
                return $this->cache[$key] = [
398
                    'isScalar'             => true,
399
                    'isNewObjectParameter' => true,
400 617
                    'fieldName'            => $this->rsm->scalarMappings[$key],
401
                    'type'                 => $this->rsm->typeMappings[$key],
402 612
                    'argIndex'             => $mapping['argIndex'],
403 612
                    'objIndex'             => $mapping['objIndex'],
404
                    'class'                => new \ReflectionClass($mapping['className']),
405
                ];
406 612
407
            case (isset($this->rsm->scalarMappings[$key])):
408 612
                return $this->cache[$key] = [
409 612
                    'isScalar'  => true,
410
                    'fieldName' => $this->rsm->scalarMappings[$key],
411 612
                    'type'      => $this->rsm->typeMappings[$key],
412 612
                ];
413 612
414
            case (isset($this->rsm->metaMappings[$key])):
415
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
416
                $fieldName = $this->rsm->metaMappings[$key];
417
                $dqlAlias  = $this->rsm->columnOwnerMap[$key];
418
419 10
                // Cache metadata fetch
420
                $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
421
422
                return $this->cache[$key] = [
423
                    'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
424
                    'isMetaColumn' => true,
425
                    'fieldName'    => $fieldName,
426
                    'type'         => $this->rsm->typeMappings[$key],
427
                    'dqlAlias'     => $dqlAlias,
428
                ];
429 878
        }
430
431 878
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
432 878
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
433
        return null;
434
    }
435 878
436
    /**
437
     * Retrieve ClassMetadata associated to entity class name.
438
     *
439
     * @param string $className
440
     *
441
     * @return \Doctrine\ORM\Mapping\ClassMetadata
442
     */
443
    protected function getClassMetadata($className)
444
    {
445
        if ( ! isset($this->metadataCache[$className])) {
446
            $this->metadataCache[$className] = $this->em->getClassMetadata($className);
447
        }
448
449 71
        return $this->metadataCache[$className];
450
    }
451
}
452