Completed
Pull Request — master (#6500)
by Mathew
17:49 queued 08:41
created

AbstractHydrator::gatherScalarRowData()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 27
c 0
b 0
f 0
ccs 15
cts 15
cp 1
rs 8.439
cc 5
eloc 15
nc 5
nop 1
crap 5
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 976
    public function __construct(EntityManagerInterface $em)
101
    {
102 976
        $this->_em       = $em;
103 976
        $this->_platform = $em->getConnection()->getDatabasePlatform();
104 976
        $this->_uow      = $em->getUnitOfWork();
105 976
    }
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 11
    public function iterate($stmt, $resultSetMapping, array $hints = [])
117
    {
118 11
        $this->_stmt  = $stmt;
119 11
        $this->_rsm   = $resultSetMapping;
120 11
        $this->_hints = $hints;
121
122 11
        $evm = $this->_em->getEventManager();
123
124 11
        $evm->addEventListener([Events::onClear], $this);
125
126 11
        $this->prepare();
127
128 11
        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 965
    public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
141
    {
142 965
        $this->_stmt  = $stmt;
143 965
        $this->_rsm   = $resultSetMapping;
144 965
        $this->_hints = $hints;
145
146 965
        $this->prepare();
147
148 964
        $result = $this->hydrateAllData();
149
150 954
        $this->cleanup();
151
152 954
        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 10
    public function hydrateRow()
162
    {
163 10
        $row = $this->_stmt->fetch(PDO::FETCH_ASSOC);
164
165 10
        if ( ! $row) {
166 7
            $this->cleanup();
167
168 7
            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 6
    public function onClear($eventArgs)
187
    {
188 6
    }
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 88
    protected function prepare()
197
    {
198 88
    }
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 961
    protected function cleanup()
207
    {
208 961
        $this->_stmt->closeCursor();
209
210 961
        $this->_stmt          = null;
211 961
        $this->_rsm           = null;
212 961
        $this->_cache         = [];
213 961
        $this->_metadataCache = [];
214 961
    }
215
216
    /**
217
     * Hydrates a single row from the current statement instance.
218
     *
219
     * Template method.
220
     *
221
     * @param array $data   The row data.
222
     * @param array $result The result to fill.
223
     *
224
     * @return void
225
     *
226
     * @throws HydrationException
227
     */
228
    protected function hydrateRowData(array $data, array &$result)
229
    {
230
        throw new HydrationException("hydrateRowData() not implemented by this hydrator.");
231
    }
232
233
    /**
234
     * Hydrates all rows from the current statement instance at once.
235
     *
236
     * @return array
237
     */
238
    abstract protected function hydrateAllData();
239
240
    /**
241
     * Processes a row of the result set.
242
     *
243
     * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
244
     * Puts the elements of a result row into a new array, grouped by the dql alias
245
     * they belong to. The column names in the result set are mapped to their
246
     * field names during this procedure as well as any necessary conversions on
247
     * the values applied. Scalar values are kept in a specific key 'scalars'.
248
     *
249
     * @param array  $data               SQL Result Row.
250
     * @param array &$id                 Dql-Alias => ID-Hash.
251
     * @param array &$nonemptyComponents Does this DQL-Alias has at least one non NULL value?
252
     *
253
     * @return array  An array with all the fields (name => value) of the data row,
254
     *                grouped by their component alias.
255
     */
256 678
    protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
257
    {
258 678
        $rowData = ['data' => []];
259
260 678
        foreach ($data as $key => $value) {
261 678
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
262 3
                continue;
263
            }
264
265 678
            $fieldName = $cacheKeyInfo['fieldName'];
266
267
            switch (true) {
268 678
                case (isset($cacheKeyInfo['isNewObjectParameter'])):
269 19
                    $argIndex = $cacheKeyInfo['argIndex'];
270 19
                    $objIndex = $cacheKeyInfo['objIndex'];
271 19
                    $type     = $cacheKeyInfo['type'];
272 19
                    $value    = $type->convertToPHPValue($value, $this->_platform);
273
274 19
                    $rowData['newObjects'][$objIndex]['class']           = $cacheKeyInfo['class'];
275 19
                    $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
276 19
                    break;
277
278 665
                case (isset($cacheKeyInfo['isScalar'])):
279 89
                    $type  = $cacheKeyInfo['type'];
280 89
                    $value = $type->convertToPHPValue($value, $this->_platform);
281
282 89
                    $rowData['scalars'][$fieldName] = $value;
283 89
                    break;
284
285
                //case (isset($cacheKeyInfo['isMetaColumn'])):
286
                default:
287 647
                    $dqlAlias = $cacheKeyInfo['dqlAlias'];
288 647
                    $type     = $cacheKeyInfo['type'];
289
290
                    // in an inheritance hierarchy the same field could be defined several times.
291
                    // We overwrite this value so long we don't have a non-null value, that value we keep.
292
                    // Per definition it cannot be that a field is defined several times and has several values.
293 647
                    if (isset($rowData['data'][$dqlAlias][$fieldName])) {
294 3
                        break;
295
                    }
296
297 647
                    $rowData['data'][$dqlAlias][$fieldName] = $type
298 647
                        ? $type->convertToPHPValue($value, $this->_platform)
299 1
                        : $value;
300
301 647
                    if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
302 647
                        $id[$dqlAlias] .= '|' . $value;
303 647
                        $nonemptyComponents[$dqlAlias] = true;
304
                    }
305 678
                    break;
306
            }
307
        }
308
309 678
        return $rowData;
310
    }
311
312
    /**
313
     * Processes a row of the result set.
314
     *
315
     * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
316
     * simply converts column names to field names and properly converts the
317
     * values according to their types. The resulting row has the same number
318
     * of elements as before.
319
     *
320
     * @param array $data
321
     *
322
     * @return array The processed row.
323
     */
324 82
    protected function gatherScalarRowData(&$data)
325
    {
326 82
        $rowData = [];
327
328 82
        foreach ($data as $key => $value) {
329 82
            if (($cacheKeyInfo = $this->hydrateColumnInfo($key)) === null) {
330 1
                continue;
331
            }
332
333 82
            $fieldName = $cacheKeyInfo['fieldName'];
334
335
            // WARNING: BC break! We know this is the desired behavior to type convert values, but this
336
            // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
337 82
            if ( ! isset($cacheKeyInfo['isScalar'])) {
338 46
                $dqlAlias  = $cacheKeyInfo['dqlAlias'];
339 46
                $type      = $cacheKeyInfo['type'];
340 46
                $fieldName = $dqlAlias . '_' . $fieldName;
341 46
                $value     = $type
342 46
                    ? $type->convertToPHPValue($value, $this->_platform)
343 46
                    : $value;
344
            }
345
346 82
            $rowData[$fieldName] = $value;
347
        }
348
349 82
        return $rowData;
350
    }
351
352
    /**
353
     * Retrieve column information from ResultSetMapping.
354
     *
355
     * @param string $key Column name
356
     *
357
     * @return array|null
358
     */
359 919
    protected function hydrateColumnInfo($key)
360
    {
361 919
        if (isset($this->_cache[$key])) {
362 411
            return $this->_cache[$key];
363
        }
364
365
        switch (true) {
366
            // NOTE: Most of the times it's a field mapping, so keep it first!!!
367 919
            case (isset($this->_rsm->fieldMappings[$key])):
368 875
                $classMetadata = $this->getClassMetadata($this->_rsm->declaringClasses[$key]);
369 875
                $fieldName     = $this->_rsm->fieldMappings[$key];
370 875
                $fieldMapping  = $classMetadata->fieldMappings[$fieldName];
371
372 875
                return $this->_cache[$key] = [
373 875
                    'isIdentifier' => in_array($fieldName, $classMetadata->identifier),
374 875
                    'fieldName'    => $fieldName,
375 875
                    'type'         => Type::getType($fieldMapping['type']),
376 875
                    'dqlAlias'     => $this->_rsm->columnOwnerMap[$key],
377
                ];
378
379 716
            case (isset($this->_rsm->newObjectMappings[$key])):
380
                // WARNING: A NEW object is also a scalar, so it must be declared before!
381 19
                $mapping = $this->_rsm->newObjectMappings[$key];
382
383 19
                return $this->_cache[$key] = [
384 19
                    'isScalar'             => true,
385
                    'isNewObjectParameter' => true,
386 19
                    'fieldName'            => $this->_rsm->scalarMappings[$key],
387 19
                    'type'                 => Type::getType($this->_rsm->typeMappings[$key]),
388 19
                    'argIndex'             => $mapping['argIndex'],
389 19
                    'objIndex'             => $mapping['objIndex'],
390 19
                    'class'                => new \ReflectionClass($mapping['className']),
391
                ];
392
393 703
            case (isset($this->_rsm->scalarMappings[$key])):
394 127
                return $this->_cache[$key] = [
395 127
                    'isScalar'  => true,
396 127
                    'fieldName' => $this->_rsm->scalarMappings[$key],
397 127
                    'type'      => Type::getType($this->_rsm->typeMappings[$key]),
398
                ];
399
400 621
            case (isset($this->_rsm->metaMappings[$key])):
401
                // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
402 616
                $fieldName = $this->_rsm->metaMappings[$key];
403 616
                $dqlAlias  = $this->_rsm->columnOwnerMap[$key];
404 616
                $type      = isset($this->_rsm->typeMappings[$key])
405 616
                    ? Type::getType($this->_rsm->typeMappings[$key])
406 616
                    : null;
407
408
                // Cache metadata fetch
409 616
                $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]);
410
411 616
                return $this->_cache[$key] = [
412 616
                    'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
413
                    'isMetaColumn' => true,
414 616
                    'fieldName'    => $fieldName,
415 616
                    'type'         => $type,
416 616
                    '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 5
        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 900
    protected function getClassMetadata($className)
433
    {
434 900
        if ( ! isset($this->_metadataCache[$className])) {
435 900
            $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
436
        }
437
438 900
        return $this->_metadataCache[$className];
439
    }
440
441
    /**
442
     * Register entity as managed in UnitOfWork.
443
     *
444
     * @param ClassMetadata $class
445
     * @param object        $entity
446
     * @param array         $data
447
     *
448
     * @return void
449
     *
450
     * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
451
     */
452 72
    protected function registerManaged(ClassMetadata $class, $entity, array $data)
453
    {
454 72
        if ($class->isIdentifierComposite) {
455 5
            $id = [];
456
457 5
            foreach ($class->identifier as $fieldName) {
458 5
                $id[$fieldName] = isset($class->associationMappings[$fieldName])
459 3
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
460 5
                    : $data[$fieldName];
461
            }
462
        } else {
463 67
            $fieldName = $class->identifier[0];
464
            $id        = [
465 67
                $fieldName => isset($class->associationMappings[$fieldName])
466
                    ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
467 67
                    : $data[$fieldName]
468
            ];
469
        }
470
471 72
        $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data);
472 72
    }
473
}
474