Failed Conditions
Pull Request — master (#7885)
by Šimon
08:56
created

AbstractHydrator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 const E_USER_DEPRECATED;
17
use function array_map;
18
use function array_merge;
19
use function array_values;
20
use function in_array;
21
use function is_array;
22
23
/**
24
 * Base class for all hydrators. A hydrator is a class that provides some form
25
 * of transformation of an SQL result set into another structure.
26
 */
27
abstract class AbstractHydrator
28
{
29
    /**
30
     * The ResultSetMapping.
31
     *
32
     * @var ResultSetMapping
33
     */
34
    protected $rsm;
35
36
    /**
37
     * The EntityManager instance.
38
     *
39
     * @var EntityManagerInterface
40
     */
41
    protected $em;
42
43
    /**
44
     * The dbms Platform instance.
45
     *
46
     * @var AbstractPlatform
47
     */
48
    protected $platform;
49
50
    /**
51
     * The UnitOfWork of the associated EntityManager.
52
     *
53
     * @var UnitOfWork
54
     */
55
    protected $uow;
56
57
    /**
58
     * Local ClassMetadata cache to avoid going to the EntityManager all the time.
59
     *
60
     * @var ClassMetadata[]
61
     */
62
    protected $metadataCache = [];
63
64
    /**
65
     * The cache used during row-by-row hydration.
66
     *
67
     * @var mixed[][]
68
     */
69
    protected $cache = [];
70
71
    /**
72
     * The statement that provides the data to hydrate.
73
     *
74
     * @var Statement
75
     */
76
    protected $stmt;
77
78
    /**
79
     * The query hints.
80
     *
81
     * @var mixed[]
82
     */
83
    protected $hints;
84
85
    /**
86
     * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
87
     *
88
     * @param EntityManagerInterface $em The EntityManager to use.
89
     */
90 992
    public function __construct(EntityManagerInterface $em)
91
    {
92 992
        $this->em       = $em;
93 992
        $this->platform = $em->getConnection()->getDatabasePlatform();
94 992
        $this->uow      = $em->getUnitOfWork();
95 992
    }
96
97
    /**
98
     * Initiates a row-by-row hydration.
99
     *
100
     * @deprecated
101
     *
102
     * @param object  $stmt
103
     * @param object  $resultSetMapping
104
     * @param mixed[] $hints
105
     *
106
     * @return IterableResult
107
     */
108 12
    public function iterate($stmt, $resultSetMapping, array $hints = [])
109
    {
110 12
        @trigger_error('Method ' . __METHOD__ . '() is deprecated and will be removed in Doctrine ORM 3.0. Use getIterable() instead.', E_USER_DEPRECATED);
0 ignored issues
show
introduced by
Function trigger_error() should not be referenced via a fallback global name, but via a use statement.
Loading history...
111
112 12
        $this->stmt  = $stmt;
113 12
        $this->rsm   = $resultSetMapping;
114 12
        $this->hints = $hints;
115
116 12
        $evm = $this->em->getEventManager();
117
118 12
        $evm->addEventListener([Events::onClear], $this);
119
120 12
        $this->prepare();
121
122 12
        return new IterableResult($this);
0 ignored issues
show
Deprecated Code introduced by
The class Doctrine\ORM\Internal\Hydration\IterableResult has been deprecated. ( Ignorable by Annotation )

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

122
        return /** @scrutinizer ignore-deprecated */ new IterableResult($this);
Loading history...
123
    }
124
125
    /**
126
     * Initiates a row-by-row hydration.
127
     *
128
     * @param mixed[] $hints
129
     *
130
     * @return iterable<mixed>
131
     */
132 25
    public function getIterable(
133
        Statement $stmt,
134
        ResultSetMapping $resultSetMapping,
135
        array $hints = []
136
    ) : iterable {
137 25
        $this->stmt  = $stmt;
138 25
        $this->rsm   = $resultSetMapping;
139 25
        $this->hints = $hints;
140
141 25
        $evm = $this->em->getEventManager();
142
143 25
        $evm->addEventListener([Events::onClear], $this);
144
145 25
        $this->prepare();
146
147 25
        while (true) {
148 25
            $row = $this->hydrateRow();
149 25
            if ($row === false) {
150 23
                break;
151
            }
152
153 23
            if (is_array($row)) {
154 23
                yield array_values($row)[0];
155
156 21
                continue;
157
            }
158
159
            yield $row;
160
        }
161 23
    }
162
163
    /**
164
     * Hydrates all rows returned by the passed statement instance at once.
165
     *
166
     * @param object  $stmt
167
     * @param object  $resultSetMapping
168
     * @param mixed[] $hints
169
     *
170
     * @return mixed[]
171
     */
172 979
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
173
    {
174 979
        $this->stmt  = $stmt;
175 979
        $this->rsm   = $resultSetMapping;
176 979
        $this->hints = $hints;
177
178 979
        $this->em->getEventManager()->addEventListener([Events::onClear], $this);
179
180 979
        $this->prepare();
181
182 978
        $result = $this->hydrateAllData();
183
184 968
        $this->cleanup();
185
186 968
        return $result;
187
    }
188
189
    /**
190
     * Hydrates a single row returned by the current statement instance during
191
     * row-by-row hydration with {@link iterate()} or {@link getIterable()}.
192
     *
193
     * @return mixed
194
     */
195 29
    public function hydrateRow()
196
    {
197 29
        $row = $this->stmt->fetch(FetchMode::ASSOCIATIVE);
198
199 29
        if ($row === false || $row === null) {
200 26
            $this->cleanup();
201
202 26
            return false;
203
        }
204
205 26
        $result = [];
206
207 26
        $this->hydrateRowData($row, $result);
208
209 26
        return $result;
210
    }
211
212
    /**
213
     * When executed in a hydrate() loop we have to clear internal state to
214
     * decrease memory consumption.
215
     *
216
     * @param mixed $eventArgs
217
     */
218 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

218
    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...
219
    {
220 8
    }
221
222
    /**
223
     * Executes one-time preparation tasks, once each time hydration is started
224
     * through {@link hydrateAll} or {@link iterate()}.
225
     */
226 106
    protected function prepare()
227
    {
228 106
    }
229
230
    /**
231
     * Executes one-time cleanup tasks at the end of a hydration that was initiated
232
     * through {@link hydrateAll} or {@link iterate()}.
233
     */
234 977
    protected function cleanup()
235
    {
236 977
        $this->stmt->closeCursor();
237
238 977
        $this->stmt          = null;
239 977
        $this->rsm           = null;
240 977
        $this->cache         = [];
241 977
        $this->metadataCache = [];
242
243 977
        $this->em
244 977
             ->getEventManager()
245 977
             ->removeEventListener([Events::onClear], $this);
246 977
    }
247
248
    /**
249
     * Hydrates a single row from the current statement instance.
250
     *
251
     * Template method.
252
     *
253
     * @param mixed[] $data   The row data.
254
     * @param mixed[] $result The result to fill.
255
     *
256
     * @throws HydrationException
257
     */
258
    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

258
    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...
259
    {
260
        throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
261
    }
262
263
    /**
264
     * Hydrates all rows from the current statement instance at once.
265
     *
266
     * @return mixed[]
267
     */
268
    abstract protected function hydrateAllData();
269
270
    /**
271
     * Processes a row of the result set.
272
     *
273
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
274
     * Puts the elements of a result row into a new array, grouped by the dql alias
275
     * they belong to. The column names in the result set are mapped to their
276
     * field names during this procedure as well as any necessary conversions on
277
     * the values applied. Scalar values are kept in a specific key 'scalars'.
278
     *
279
     * @param mixed[] $data               SQL Result Row.
280
     * @param mixed[] $id                 Dql-Alias => ID-Hash.
281
     * @param mixed[] $nonemptyComponents Does this DQL-Alias has at least one non NULL value?
282
     *
283
     * @return mixed[] An array with all the fields (name => value) of the data row,
284
     *                grouped by their component alias.
285
     */
286 698
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
287
    {
288 698
        $rowData = ['data' => []];
289
290 698
        foreach ($data as $key => $value) {
291 698
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
292 698
            if ($cacheKeyInfo === null) {
293 9
                continue;
294
            }
295
296 698
            $fieldName = $cacheKeyInfo['fieldName'];
297
298
            switch (true) {
299 698
                case isset($cacheKeyInfo['isNewObjectParameter']):
300 19
                    $argIndex = $cacheKeyInfo['argIndex'];
301 19
                    $objIndex = $cacheKeyInfo['objIndex'];
302 19
                    $type     = $cacheKeyInfo['type'];
303 19
                    $value    = $type->convertToPHPValue($value, $this->platform);
304
305 19
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
306 19
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
307 19
                    break;
308
309 685
                case isset($cacheKeyInfo['isScalar']):
310 113
                    $type  = $cacheKeyInfo['type'];
311 113
                    $value = $type->convertToPHPValue($value, $this->platform);
312
313 113
                    $rowData['scalars'][$fieldName] = $value;
314 113
                    break;
315
316
                //case (isset($cacheKeyInfo['isMetaColumn'])):
317
                default:
318 644
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
319 644
                    $type     = $cacheKeyInfo['type'];
320
321
                    // If there are field name collisions in the child class, then we need
322
                    // to only hydrate if we are looking at the correct discriminator value
323 644
                    if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
324 644
                        && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
325
                    ) {
326 24
                        break;
327
                    }
328
329
                    // in an inheritance hierarchy the same field could be defined several times.
330
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
331
                    // Per definition it cannot be that a field is defined several times and has several values.
332 644
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
333
                        break;
334
                    }
335
336 644
                    $rowData['data'][$dqlAlias][$fieldName] = $type
337 644
                        ? $type->convertToPHPValue($value, $this->platform)
338
                        : $value;
339
340 644
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
341 644
                        $id[$dqlAlias]                .= '|' . $value;
342 644
                        $nonemptyComponents[$dqlAlias] = true;
343
                    }
344 644
                    break;
345
            }
346
        }
347
348 698
        return $rowData;
349
    }
350
351
    /**
352
     * Processes a row of the result set.
353
     *
354
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
355
     * simply converts column names to field names and properly converts the
356
     * values according to their types. The resulting row has the same number
357
     * of elements as before.
358
     *
359
     * @param mixed[] $data
360
     *
361
     * @return mixed[] The processed row.
362
     */
363 98
    protected function gatherScalarRowData(&$data)
364
    {
365 98
        $rowData = [];
366
367 98
        foreach ($data as $key => $value) {
368 98
            $cacheKeyInfo = $this->hydrateColumnInfo($key);
369 98
            if ($cacheKeyInfo === null) {
370 1
                continue;
371
            }
372
373 98
            $fieldName = $cacheKeyInfo['fieldName'];
374
375
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
376
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
377 98
            if (! isset($cacheKeyInfo['isScalar'])) {
378 49
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
379 49
                $type      = $cacheKeyInfo['type'];
380 49
                $fieldName = $dqlAlias . '_' . $fieldName;
381 49
                $value     = $type
382 49
                    ? $type->convertToPHPValue($value, $this->platform)
383 49
                    : $value;
384
            }
385
386 98
            $rowData[$fieldName] = $value;
387
        }
388
389 98
        return $rowData;
390
    }
391
392
    /**
393
     * Retrieve column information from ResultSetMapping.
394
     *
395
     * @param string $key Column name
396
     *
397
     * @return mixed[]|null
398
     */
399 932
    protected function hydrateColumnInfo($key)
400
    {
401 932
        if (isset($this->cache[$key])) {
402 424
            return $this->cache[$key];
403
        }
404
405
        switch (true) {
406
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
407 932
            case isset($this->rsm->fieldMappings[$key]):
408 854
                $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
409 854
                $fieldName     = $this->rsm->fieldMappings[$key];
410 854
                $ownerMap      = $this->rsm->columnOwnerMap[$key];
411 854
                $property      = $classMetadata->getProperty($fieldName);
412
413
                $columnInfo = [
414 854
                    'isIdentifier' => $property->isPrimaryKey(),
415 854
                    'fieldName'    => $fieldName,
416 854
                    '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

416
                    'type'         => $property->/** @scrutinizer ignore-call */ getType(),
Loading history...
417 854
                    'dqlAlias'     => $this->rsm->columnOwnerMap[$key],
418
                ];
419
420
                // the current discriminator value must be saved in order to disambiguate fields hydration,
421
                // should there be field name collisions
422 854
                if ($classMetadata->getParent() && isset($this->rsm->discriminatorColumns[$ownerMap])) {
423 108
                    return $this->cache[$key] = array_merge(
424 108
                        $columnInfo,
425
                        [
426 108
                            'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
427 108
                            'discriminatorValue'  => $classMetadata->discriminatorValue,
428 108
                            'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
429
                        ]
430
                    );
431
                }
432
433 825
                return $this->cache[$key] = $columnInfo;
434 743
            case isset($this->rsm->newObjectMappings[$key]):
435
                // WARNING: A NEW object is also a scalar, so it must be declared before!
436 19
                $mapping = $this->rsm->newObjectMappings[$key];
437
438 19
                return $this->cache[$key] = [
439 19
                    'isScalar'             => true,
440
                    'isNewObjectParameter' => true,
441 19
                    'fieldName'            => $this->rsm->scalarMappings[$key],
442 19
                    'type'                 => $this->rsm->typeMappings[$key],
443 19
                    'argIndex'             => $mapping['argIndex'],
444 19
                    'objIndex'             => $mapping['objIndex'],
445 19
                    'class'                => new ReflectionClass($mapping['className']),
446
                ];
447 730
            case isset($this->rsm->scalarMappings[$key]):
448 163
                return $this->cache[$key] = [
449 163
                    'isScalar'  => true,
450 163
                    'fieldName' => $this->rsm->scalarMappings[$key],
451 163
                    'type'      => $this->rsm->typeMappings[$key],
452
                ];
453 614
            case isset($this->rsm->metaMappings[$key]):
454
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
455 609
                $fieldName = $this->rsm->metaMappings[$key];
456 609
                $dqlAlias  = $this->rsm->columnOwnerMap[$key];
457
458
                // Cache metadata fetch
459 609
                $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
460
461 609
                return $this->cache[$key] = [
462 609
                    'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
463
                    'isMetaColumn' => true,
464 609
                    'fieldName'    => $fieldName,
465 609
                    'type'         => $this->rsm->typeMappings[$key],
466 609
                    'dqlAlias'     => $dqlAlias,
467
                ];
468
        }
469
470
        // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
471
        // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
472 11
        return null;
473
    }
474
475
    /**
476
     * @return string[]
477
     */
478 108
    private function getDiscriminatorValues(ClassMetadata $classMetadata) : array
479
    {
480 108
        $values = array_map(
481
            function (string $subClass) : string {
482 50
                return (string) $this->getClassMetadata($subClass)->discriminatorValue;
483 108
            },
484 108
            $classMetadata->getSubClasses()
485
        );
486
487 108
        $values[] = (string) $classMetadata->discriminatorValue;
488
489 108
        return $values;
490
    }
491
492
    /**
493
     * Retrieve ClassMetadata associated to entity class name.
494
     *
495
     * @param string $className
496
     *
497
     * @return ClassMetadata
498
     */
499 875
    protected function getClassMetadata($className)
500
    {
501 875
        if (! isset($this->metadataCache[$className])) {
502 875
            $this->metadataCache[$className] = $this->em->getClassMetadata($className);
503
        }
504
505 875
        return $this->metadataCache[$className];
506
    }
507
}
508