Failed Conditions
Pull Request — 2.7 (#8027)
by
unknown
07:00
created

AbstractHydrator   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 423
Duplicated Lines 0 %

Test Coverage

Coverage 47.55%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 35
eloc 142
c 7
b 0
f 0
dl 0
loc 423
ccs 68
cts 143
cp 0.4755
rs 9.6

12 Methods

Rating   Name   Duplication   Size   Complexity  
A onClear() 0 2 1
A prepare() 0 2 1
A hydrateRowData() 0 3 1
A getClassMetadata() 0 7 2
A hydrateAll() 0 15 1
A gatherScalarRowData() 0 27 5
A cleanup() 0 12 1
A iterate() 0 13 1
B gatherRowData() 0 64 11
A hydrateRow() 0 15 2
B hydrateColumnInfo() 0 76 8
A __construct() 0 5 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 Doctrine\ORM\Mapping\ClassMetadata;
10
use PDO;
11
12
/**
13
 * Base class for all hydrators. A hydrator is a class that provides some form
14
 * of transformation of an SQL result set into another structure.
15
 */
16
abstract class AbstractHydrator
17
{
18
    /**
19
     * The ResultSetMapping.
20
     *
21
     * @var \Doctrine\ORM\Query\ResultSetMapping
22
     */
23
    protected $rsm;
24
25
    /**
26
     * The EntityManager instance.
27
     *
28
     * @var EntityManagerInterface
29
     */
30
    protected $em;
31
32
    /**
33
     * The dbms Platform instance.
34
     *
35
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
36
     */
37
    protected $platform;
38
39
    /**
40
     * The UnitOfWork of the associated EntityManager.
41
     *
42
     * @var \Doctrine\ORM\UnitOfWork
43
     */
44
    protected $uow;
45
46
    /**
47
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
48
     *
49
     * @var ClassMetadata[]
50
     */
51
    protected $metadataCache = [];
52
53
    /**
54
     * The cache used during row-by-row hydration.
55
     *
56
     * @var mixed[][]
57
     */
58
    protected $cache = [];
59
60
    /**
61
     * The statement that provides the data to hydrate.
62
     *
63
     * @var \Doctrine\DBAL\Driver\Statement
64
     */
65
    protected $stmt;
66
67
    /**
68
     * The query hints.
69
     *
70
     * @var mixed[]
71
     */
72
    protected $hints;
73
74
    /**
75
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
76
     *
77
     * @param EntityManagerInterface $em The EntityManager to use.
78
     */
79 208
    public function __construct(EntityManagerInterface $em)
80
    {
81 208
        $this->em       = $em;
82 208
        $this->platform = $em->getConnection()->getDatabasePlatform();
83 208
        $this->uow      = $em->getUnitOfWork();
84 208
    }
85
86
    /**
87
     * Initiates a row-by-row hydration.
88
     *
89
     * @param object  $stmt
90
     * @param object  $resultSetMapping
91
     * @param mixed[] $hints
92
     *
93
     * @return IterableResult
94
     */
95 2
    public function iterate($stmt, $resultSetMapping, array $hints = [])
96
    {
97 2
        $this->stmt  = $stmt;
98 2
        $this->rsm   = $resultSetMapping;
99 2
        $this->hints = $hints;
100
101 2
        $evm = $this->em->getEventManager();
102
103 2
        $evm->addEventListener([Events::onClear], $this);
104
105 2
        $this->prepare();
106
107 2
        return new IterableResult($this);
108
    }
109
110
    /**
111
     * Hydrates all rows returned by the passed statement instance at once.
112
     *
113
     * @param object  $stmt
114
     * @param object  $resultSetMapping
115
     * @param mixed[] $hints
116
     *
117
     * @return mixed[]
118
     */
119 205
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
120
    {
121 205
        $this->stmt  = $stmt;
122 205
        $this->rsm   = $resultSetMapping;
123 205
        $this->hints = $hints;
124
125 205
        $this->em->getEventManager()->addEventListener([Events::onClear], $this);
126
127 205
        $this->prepare();
128
129 164
        $result = $this->hydrateAllData();
130
131 54
        $this->cleanup();
132
133 54
        return $result;
134
    }
135
136
    /**
137
     * Hydrates a single row returned by the current statement instance during
138
     * row-by-row hydration with {@link iterate()}.
139
     *
140
     * @return mixed
141
     */
142 1
    public function hydrateRow()
143
    {
144 1
        $row = $this->stmt->fetch(PDO::FETCH_ASSOC);
145
146 1
        if (! $row) {
147 1
            $this->cleanup();
148
149 1
            return false;
150
        }
151
152
        $result = [];
153
154
        $this->hydrateRowData($row, $result);
155
156
        return $result;
157
    }
158
159
    /**
160
     * When executed in a hydrate() loop we have to clear internal state to
161
     * decrease memory consumption.
162
     *
163
     * @param mixed $eventArgs
164
     */
165 145
    public function onClear($eventArgs)
166
    {
167 145
    }
168
169
    /**
170
     * Executes one-time preparation tasks, once each time hydration is started
171
     * through {@link hydrateAll} or {@link iterate()}.
172
     */
173 21
    protected function prepare()
174
    {
175 21
    }
176
177
    /**
178
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
179
     * through {@link hydrateAll} or {@link iterate()}.
180
     */
181 55
    protected function cleanup()
182
    {
183 55
        $this->stmt->closeCursor();
184
185 55
        $this->stmt          = null;
186 55
        $this->rsm           = null;
187 55
        $this->cache         = [];
188 55
        $this->metadataCache = [];
189
190 55
        $this->em
191 55
             ->getEventManager()
192 55
             ->removeEventListener([Events::onClear], $this);
193 55
    }
194
195
    /**
196
     * Hydrates a single row from the current statement instance.
197
     *
198
     * Template method.
199
     *
200
     * @param mixed[] $data   The row data.
201
     * @param mixed[] $result The result to fill.
202
     *
203
     * @throws HydrationException
204
     */
205
    protected function hydrateRowData(array $data, array &$result)
206
    {
207
        throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
208
    }
209
210
    /**
211
     * Hydrates all rows from the current statement instance at once.
212
     *
213
     * @return mixed[]
214
     */
215
    abstract protected function hydrateAllData();
216
217
    /**
218
     * Processes a row of the result set.
219
     *
220
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
221
     * Puts the elements of a result row into a new array, grouped by the dql alias
222
     * they belong to. The column names in the result set are mapped to their
223
     * field names during this procedure as well as any necessary conversions on
224
     * the values applied. Scalar values are kept in a specific key 'scalars'.
225
     *
226
     * @param mixed[] $data                SQL Result Row.
227
     * @param mixed[] &$id                 Dql-Alias => ID-Hash.
228
     * @param mixed[] &$nonemptyComponents Does this DQL-Alias has at least one non NULL value?
229
     *
230
     * @return mixed[] An array with all the fields (name => value) of the data row,
231
     *                grouped by their component alias.
232
     */
233 31
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
234
    {
235 31
        $rowData = ['data' => []];
236
237 31
        foreach ($data as $key => $value) {
238 31
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
239 1
            if ($cacheKeyInfo === null) {
240
                continue;
241
            }
242
243 1
            $fieldName = $cacheKeyInfo['fieldName'];
244
245
            switch (true) {
246 1
                case (isset($cacheKeyInfo['isNewObjectParameter'])):
247
                    $argIndex = $cacheKeyInfo['argIndex'];
248
                    $objIndex = $cacheKeyInfo['objIndex'];
249
                    $type     = $cacheKeyInfo['type'];
250
                    $value    = $type->convertToPHPValue($value, $this->platform);
251
252
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
253
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
254
                    break;
255
256 1
                case (isset($cacheKeyInfo['isScalar'])):
257 1
                    $type  = $cacheKeyInfo['type'];
258 1
                    $value = $type->convertToPHPValue($value, $this->platform);
259
260
                    $rowData['scalars'][$fieldName] = $value;
261
                    break;
262
263
                //case (isset($cacheKeyInfo['isMetaColumn'])):
264
                default:
265
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
266
                    $type     = $cacheKeyInfo['type'];
267
268
                    // If there are field name collisions in the child class, then we need
269
                    // to only hydrate if we are looking at the correct discriminator value
270
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']]) &&
271
                        // Note: loose comparison required. See https://github.com/doctrine/doctrine2/pull/6304#issuecomment-323294442
272
                        $data[$cacheKeyInfo['discriminatorColumn']] != $cacheKeyInfo['discriminatorValue'] // TODO get rid of loose comparison
273
                    ) {
274
                        break;
275
                    }
276
277
                    // in an inheritance hierarchy the same field could be defined several times.
278
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
279
                    // Per definition it cannot be that a field is defined several times and has several values.
280
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
281
                        break;
282
                    }
283
284
                    $rowData['data'][$dqlAlias][$fieldName] = $type
285
                        ? $type->convertToPHPValue($value, $this->platform)
286
                        : $value;
287
288
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
289
                        $id[$dqlAlias]                .= '|' . $value;
290
                        $nonemptyComponents[$dqlAlias] = true;
291
                    }
292
                    break;
293
            }
294
        }
295
296
        return $rowData;
297
    }
298
299
    /**
300
     * Processes a row of the result set.
301
     *
302
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
303
     * simply converts column names to field names and properly converts the
304
     * values according to their types. The resulting row has the same number
305
     * of elements as before.
306
     *
307
     * @param mixed[] $data
308
     *
309
     * @return mixed[] The processed row.
310
     */
311
    protected function gatherScalarRowData(&$data)
312
    {
313
        $rowData = [];
314
315
        foreach ($data as $key => $value) {
316
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
317
            if ($cacheKeyInfo === null) {
318
                continue;
319
            }
320
321
            $fieldName = $cacheKeyInfo['fieldName'];
322
323
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
324
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
325
            if (! isset($cacheKeyInfo['isScalar'])) {
326
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
327
                $type      = $cacheKeyInfo['type'];
328
                $fieldName = $dqlAlias . '_' . $fieldName;
329
                $value     = $type
330
                    ? $type->convertToPHPValue($value, $this->platform)
331
                    : $value;
332
            }
333
334
            $rowData[$fieldName] = $value;
335
        }
336
337
        return $rowData;
338
    }
339
340
    /**
341
     * Retrieve column information from ResultSetMapping.
342
     *
343
     * @param string $key Column name
344
     *
345
     * @return mixed[]|null
346
     */
347 31
    protected function hydrateColumnInfo($key)
348
    {
349 31
        if (isset($this->cache[$key])) {
350
            return $this->cache[$key];
351
        }
352
353
        switch (true) {
354
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
355 31
            case (isset($this->rsm->fieldMappings[$key])):
356 30
                $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
357 30
                $fieldName     = $this->rsm->fieldMappings[$key];
358 30
                $ownerMap      = $this->rsm->columnOwnerMap[$key];
359 30
                $property      = $classMetadata->getProperty($fieldName);
0 ignored issues
show
Bug introduced by
The method getProperty() does not exist on Doctrine\ORM\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

359
                /** @scrutinizer ignore-call */ 
360
                $property      = $classMetadata->getProperty($fieldName);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
360
361
                $columnInfo = [
362
                    'isIdentifier' => $property->isPrimaryKey(),
363
                    'fieldName'    => $fieldName,
364
                    'type'         => $property->getType(),
365
                    'dqlAlias'     => $this->rsm->columnOwnerMap[$key],
366
                ];
367
368
                // the current discriminator value must be saved in order to disambiguate fields hydration,
369
                // should there be field name collisions
370
                if ($classMetadata->getParent() && isset($this->rsm->discriminatorColumns[$ownerMap])) {
0 ignored issues
show
Bug introduced by
The method getParent() does not exist on Doctrine\ORM\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

370
                if ($classMetadata->/** @scrutinizer ignore-call */ getParent() && isset($this->rsm->discriminatorColumns[$ownerMap])) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
371
                    return $this->cache[$key] = \array_merge(
372
                        $columnInfo,
373
                        [
374
                            'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
375
                            'discriminatorValue'  => $classMetadata->discriminatorValue,
376
                        ]
377
                    );
378
                }
379
380
                return $this->cache[$key] = $columnInfo;
381
382 1
            case (isset($this->rsm->newObjectMappings[$key])):
383
                // WARNING: A NEW object is also a scalar, so it must be declared before!
384
                $mapping = $this->rsm->newObjectMappings[$key];
385
386
                return $this->cache[$key] = [
387
                    'isScalar'             => true,
388
                    'isNewObjectParameter' => true,
389
                    'fieldName'            => $this->rsm->scalarMappings[$key],
390
                    'type'                 => $this->rsm->typeMappings[$key],
391
                    'argIndex'             => $mapping['argIndex'],
392
                    'objIndex'             => $mapping['objIndex'],
393
                    'class'                => new \ReflectionClass($mapping['className']),
394
                ];
395
396 1
            case (isset($this->rsm->scalarMappings[$key])):
397 1
                return $this->cache[$key] = [
398 1
                    'isScalar'  => true,
399 1
                    'fieldName' => $this->rsm->scalarMappings[$key],
400 1
                    'type'      => $this->rsm->typeMappings[$key],
401
                ];
402
403
            case (isset($this->rsm->metaMappings[$key])):
404
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
405
                $fieldName = $this->rsm->metaMappings[$key];
406
                $dqlAlias  = $this->rsm->columnOwnerMap[$key];
407
408
                // Cache metadata fetch
409
                $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
410
411
                return $this->cache[$key] = [
412
                    'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
413
                    'isMetaColumn' => true,
414
                    'fieldName'    => $fieldName,
415
                    'type'         => $this->rsm->typeMappings[$key],
416
                    'dqlAlias'     => $dqlAlias,
417
                ];
418
        }
419
420
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
421
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
422
        return null;
423
    }
424
425
    /**
426
     * Retrieve ClassMetadata associated to entity class name.
427
     *
428
     * @param string $className
429
     *
430
     * @return \Doctrine\ORM\Mapping\ClassMetadata
431
     */
432 150
    protected function getClassMetadata($className)
433
    {
434 150
        if (! isset($this->metadataCache[$className])) {
435 150
            $this->metadataCache[$className] = $this->em->getClassMetadata($className);
436
        }
437
438 150
        return $this->metadataCache[$className];
439
    }
440
}
441