AbstractHydrator   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 437
Duplicated Lines 0 %

Test Coverage

Coverage 97.35%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 148
c 3
b 0
f 0
dl 0
loc 437
ccs 147
cts 151
cp 0.9735
rs 9.52
wmc 36

13 Methods

Rating   Name   Duplication   Size   Complexity  
A prepare() 0 2 1
A onClear() 0 2 1
A hydrateAll() 0 15 1
A gatherScalarRowData() 0 27 5
A cleanup() 0 12 1
A hydrateRowData() 0 3 1
A iterate() 0 13 1
A hydrateRow() 0 15 2
A __construct() 0 5 1
A getClassMetadata() 0 7 2
A getDiscriminatorValues() 0 12 1
B gatherRowData() 0 63 11
B hydrateColumnInfo() 0 74 8
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Internal\Hydration;
6
7
use Doctrine\DBAL\Driver\Statement;
8
use Doctrine\DBAL\FetchMode;
9
use Doctrine\DBAL\Platforms\AbstractPlatform;
10
use Doctrine\ORM\EntityManagerInterface;
11
use Doctrine\ORM\Events;
12
use Doctrine\ORM\Mapping\ClassMetadata;
13
use Doctrine\ORM\Query\ResultSetMapping;
14
use Doctrine\ORM\UnitOfWork;
15
use ReflectionClass;
16
use function array_map;
17
use function array_merge;
18
use function in_array;
19
20
/**
21
 * Base class for all hydrators. A hydrator is a class that provides some form
22
 * of transformation of an SQL result set into another structure.
23
 */
24
abstract class AbstractHydrator
25
{
26
    /**
27
     * The ResultSetMapping.
28
     *
29
     * @var ResultSetMapping
30
     */
31
    protected $rsm;
32
33
    /**
34
     * The EntityManager instance.
35
     *
36
     * @var EntityManagerInterface
37
     */
38
    protected $em;
39
40
    /**
41
     * The dbms Platform instance.
42
     *
43
     * @var AbstractPlatform
44
     */
45
    protected $platform;
46
47
    /**
48
     * The UnitOfWork of the associated EntityManager.
49
     *
50
     * @var UnitOfWork
51
     */
52
    protected $uow;
53
54
    /**
55
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
56
     *
57
     * @var ClassMetadata[]
58
     */
59
    protected $metadataCache = [];
60
61
    /**
62
     * The cache used during row-by-row hydration.
63
     *
64
     * @var mixed[][]
65
     */
66
    protected $cache = [];
67
68
    /**
69
     * The statement that provides the data to hydrate.
70
     *
71
     * @var Statement
72
     */
73
    protected $stmt;
74
75
    /**
76
     * The query hints.
77
     *
78
     * @var mixed[]
79
     */
80
    protected $hints;
81
82
    /**
83
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
84
     *
85
     * @param EntityManagerInterface $em The EntityManager to use.
86
     */
87 989
    public function __construct(EntityManagerInterface $em)
88
    {
89 989
        $this->em       = $em;
90 989
        $this->platform = $em->getConnection()->getDatabasePlatform();
91 989
        $this->uow      = $em->getUnitOfWork();
92 989
    }
93
94
    /**
95
     * Initiates a row-by-row hydration.
96
     *
97
     * @param object  $stmt
98
     * @param object  $resultSetMapping
99
     * @param mixed[] $hints
100
     *
101
     * @return IterableResult
102
     */
103 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
104
    {
105 12
        $this->stmt  = $stmt;
106 12
        $this->rsm   = $resultSetMapping;
107 12
        $this->hints = $hints;
108
109 12
        $evm = $this->em->getEventManager();
110
111 12
        $evm->addEventListener([Events::onClear], $this);
112
113 12
        $this->prepare();
114
115 12
        return new IterableResult($this);
116
    }
117
118
    /**
119
     * Hydrates all rows returned by the passed statement instance at once.
120
     *
121
     * @param object  $stmt
122
     * @param object  $resultSetMapping
123
     * @param mixed[] $hints
124
     *
125
     * @return mixed[]
126
     */
127 977
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
128
    {
129 977
        $this->stmt  = $stmt;
130 977
        $this->rsm   = $resultSetMapping;
131 977
        $this->hints = $hints;
132
133 977
        $this->em->getEventManager()->addEventListener([Events::onClear], $this);
134
135 977
        $this->prepare();
136
137 976
        $result = $this->hydrateAllData();
138
139 966
        $this->cleanup();
140
141 966
        return $result;
142
    }
143
144
    /**
145
     * Hydrates a single row returned by the current statement instance during
146
     * row-by-row hydration with {@link iterate()}.
147
     *
148
     * @return mixed
149
     */
150 11
    public function hydrateRow()
151
    {
152 11
        $row = $this->stmt->fetch(FetchMode::ASSOCIATIVE);
153
154 11
        if (! $row) {
155 8
            $this->cleanup();
156
157 8
            return false;
158
        }
159
160 10
        $result = [];
161
162 10
        $this->hydrateRowData($row, $result);
163
164 10
        return $result;
165
    }
166
167
    /**
168
     * When executed in a hydrate() loop we have to clear internal state to
169
     * decrease memory consumption.
170
     *
171
     * @param mixed $eventArgs
172
     */
173 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

173
    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...
174
    {
175 8
    }
176
177
    /**
178
     * Executes one-time preparation tasks, once each time hydration is started
179
     * through {@link hydrateAll} or {@link iterate()}.
180
     */
181 106
    protected function prepare()
182
    {
183 106
    }
184
185
    /**
186
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
187
     * through {@link hydrateAll} or {@link iterate()}.
188
     */
189 974
    protected function cleanup()
190
    {
191 974
        $this->stmt->closeCursor();
192
193 974
        $this->stmt          = null;
194 974
        $this->rsm           = null;
195 974
        $this->cache         = [];
196 974
        $this->metadataCache = [];
197
198 974
        $this->em
199 974
             ->getEventManager()
200 974
             ->removeEventListener([Events::onClear], $this);
201 974
    }
202
203
    /**
204
     * Hydrates a single row from the current statement instance.
205
     *
206
     * Template method.
207
     *
208
     * @param mixed[] $data   The row data.
209
     * @param mixed[] $result The result to fill.
210
     *
211
     * @throws HydrationException
212
     */
213
    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

213
    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...
214
    {
215
        throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
216
    }
217
218
    /**
219
     * Hydrates all rows from the current statement instance at once.
220
     *
221
     * @return mixed[]
222
     */
223
    abstract protected function hydrateAllData();
224
225
    /**
226
     * Processes a row of the result set.
227
     *
228
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
229
     * Puts the elements of a result row into a new array, grouped by the dql alias
230
     * they belong to. The column names in the result set are mapped to their
231
     * field names during this procedure as well as any necessary conversions on
232
     * the values applied. Scalar values are kept in a specific key 'scalars'.
233
     *
234
     * @param mixed[] $data               SQL Result Row.
235
     * @param mixed[] $id                 Dql-Alias => ID-Hash.
236
     * @param mixed[] $nonemptyComponents Does this DQL-Alias has at least one non NULL value?
237
     *
238
     * @return mixed[] An array with all the fields (name => value) of the data row,
239
     *                grouped by their component alias.
240
     */
241 695
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
242
    {
243 695
        $rowData = ['data' => []];
244
245 695
        foreach ($data as $key => $value) {
246 695
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
247 695
            if ($cacheKeyInfo === null) {
248 8
                continue;
249
            }
250
251 695
            $fieldName = $cacheKeyInfo['fieldName'];
252
253
            switch (true) {
254 695
                case isset($cacheKeyInfo['isNewObjectParameter']):
255 19
                    $argIndex = $cacheKeyInfo['argIndex'];
256 19
                    $objIndex = $cacheKeyInfo['objIndex'];
257 19
                    $type     = $cacheKeyInfo['type'];
258 19
                    $value    = $type->convertToPHPValue($value, $this->platform);
259
260 19
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
261 19
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
262 19
                    break;
263
264 682
                case isset($cacheKeyInfo['isScalar']):
265 112
                    $type  = $cacheKeyInfo['type'];
266 112
                    $value = $type->convertToPHPValue($value, $this->platform);
267
268 112
                    $rowData['scalars'][$fieldName] = $value;
269 112
                    break;
270
271
                //case (isset($cacheKeyInfo['isMetaColumn'])):
272
                default:
273 642
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
274 642
                    $type     = $cacheKeyInfo['type'];
275
276
                    // If there are field name collisions in the child class, then we need
277
                    // to only hydrate if we are looking at the correct discriminator value
278 642
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
279 642
                        && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
280
                    ) {
281 24
                        break;
282
                    }
283
284
                    // in an inheritance hierarchy the same field could be defined several times.
285
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
286
                    // Per definition it cannot be that a field is defined several times and has several values.
287 642
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
288
                        break;
289
                    }
290
291 642
                    $rowData['data'][$dqlAlias][$fieldName] = $type
292 642
                        ? $type->convertToPHPValue($value, $this->platform)
293
                        : $value;
294
295 642
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
296 642
                        $id[$dqlAlias]                .= '|' . $value;
297 642
                        $nonemptyComponents[$dqlAlias] = true;
298
                    }
299 642
                    break;
300
            }
301
        }
302
303 695
        return $rowData;
304
    }
305
306
    /**
307
     * Processes a row of the result set.
308
     *
309
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
310
     * simply converts column names to field names and properly converts the
311
     * values according to their types. The resulting row has the same number
312
     * of elements as before.
313
     *
314
     * @param mixed[] $data
315
     *
316
     * @return mixed[] The processed row.
317
     */
318 98
    protected function gatherScalarRowData(&$data)
319
    {
320 98
        $rowData = [];
321
322 98
        foreach ($data as $key => $value) {
323 98
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
324 98
            if ($cacheKeyInfo === null) {
325 1
                continue;
326
            }
327
328 98
            $fieldName = $cacheKeyInfo['fieldName'];
329
330
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
331
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
332 98
            if (! isset($cacheKeyInfo['isScalar'])) {
333 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
334 49
                $type      = $cacheKeyInfo['type'];
335 49
                $fieldName = $dqlAlias . '_' . $fieldName;
336 49
                $value     = $type
337 49
                    ? $type->convertToPHPValue($value, $this->platform)
338 49
                    : $value;
339
            }
340
341 98
            $rowData[$fieldName] = $value;
342
        }
343
344 98
        return $rowData;
345
    }
346
347
    /**
348
     * Retrieve column information from ResultSetMapping.
349
     *
350
     * @param string $key Column name
351
     *
352
     * @return mixed[]|null
353
     */
354 930
    protected function hydrateColumnInfo($key)
355
    {
356 930
        if (isset($this->cache[$key])) {
357 424
            return $this->cache[$key];
358
        }
359
360
        switch (true) {
361
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
362 930
            case isset($this->rsm->fieldMappings[$key]):
363 852
                $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
364 852
                $fieldName     = $this->rsm->fieldMappings[$key];
365 852
                $ownerMap      = $this->rsm->columnOwnerMap[$key];
366 852
                $property      = $classMetadata->getProperty($fieldName);
367
368
                $columnInfo = [
369 852
                    'isIdentifier' => $property->isPrimaryKey(),
370 852
                    'fieldName'    => $fieldName,
371 852
                    'type'         => $property->getType(),
0 ignored issues
show
Bug introduced by
The method getType() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

371
                    'type'         => $property->/** @scrutinizer ignore-call */ getType(),
Loading history...
372 852
                    'dqlAlias'     => $this->rsm->columnOwnerMap[$key],
373
                ];
374
375
                // the current discriminator value must be saved in order to disambiguate fields hydration,
376
                // should there be field name collisions
377 852
                if ($classMetadata->getParent() && isset($this->rsm->discriminatorColumns[$ownerMap])) {
378 108
                    return $this->cache[$key] = array_merge(
379 108
                        $columnInfo,
380
                        [
381 108
                            'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
382 108
                            'discriminatorValue'  => $classMetadata->discriminatorValue,
383 108
                            'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
384
                        ]
385
                    );
386
                }
387
388 823
                return $this->cache[$key] = $columnInfo;
389 741
            case isset($this->rsm->newObjectMappings[$key]):
390
                // WARNING: A NEW object is also a scalar, so it must be declared before!
391 19
                $mapping = $this->rsm->newObjectMappings[$key];
392
393 19
                return $this->cache[$key] = [
394 19
                    'isScalar'             => true,
395
                    'isNewObjectParameter' => true,
396 19
                    'fieldName'            => $this->rsm->scalarMappings[$key],
397 19
                    'type'                 => $this->rsm->typeMappings[$key],
398 19
                    'argIndex'             => $mapping['argIndex'],
399 19
                    'objIndex'             => $mapping['objIndex'],
400 19
                    'class'                => new ReflectionClass($mapping['className']),
401
                ];
402 728
            case isset($this->rsm->scalarMappings[$key]):
403 163
                return $this->cache[$key] = [
404 163
                    'isScalar'  => true,
405 163
                    'fieldName' => $this->rsm->scalarMappings[$key],
406 163
                    'type'      => $this->rsm->typeMappings[$key],
407
                ];
408 612
            case isset($this->rsm->metaMappings[$key]):
409
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
410 607
                $fieldName = $this->rsm->metaMappings[$key];
411 607
                $dqlAlias  = $this->rsm->columnOwnerMap[$key];
412
413
                // Cache metadata fetch
414 607
                $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
415
416 607
                return $this->cache[$key] = [
417 607
                    'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
418
                    'isMetaColumn' => true,
419 607
                    'fieldName'    => $fieldName,
420 607
                    'type'         => $this->rsm->typeMappings[$key],
421 607
                    'dqlAlias'     => $dqlAlias,
422
                ];
423
        }
424
425
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
426
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
427 10
        return null;
428
    }
429
430
    /**
431
     * @return string[]
432
     */
433 108
    private function getDiscriminatorValues(ClassMetadata $classMetadata) : array
434
    {
435 108
        $values = array_map(
436
            function (string $subClass) : string {
437 50
                return (string) $this->getClassMetadata($subClass)->discriminatorValue;
438 108
            },
439 108
            $classMetadata->getSubClasses()
440
        );
441
442 108
        $values[] = (string) $classMetadata->discriminatorValue;
443
444 108
        return $values;
445
    }
446
447
    /**
448
     * Retrieve ClassMetadata associated to entity class name.
449
     *
450
     * @param string $className
451
     *
452
     * @return ClassMetadata
453
     */
454 873
    protected function getClassMetadata($className)
455
    {
456 873
        if (! isset($this->metadataCache[$className])) {
457 873
            $this->metadataCache[$className] = $this->em->getClassMetadata($className);
458
        }
459
460 873
        return $this->metadataCache[$className];
461
    }
462
}
463