Failed Conditions
Pull Request — master (#6143)
by Luís
10:34
created

AbstractHydrator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
463 5
                $id[$fieldName] = isset($class->associationMappings[$fieldName])
464 3
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
465 5
                    : $data[$fieldName];
466
            }
467
        } else {
468 67
            $fieldName = $class->identifier[0];
469
            $id        = [
470 67
                $fieldName => isset($class->associationMappings[$fieldName])
471
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
472 67
                    : $data[$fieldName]
473
            ];
474
        }
475
476 72
        $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data);
477 72
    }
478
}
479