1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Doctrine\ORM\Internal\Hydration; |
6
|
|
|
|
7
|
|
|
use Doctrine\ORM\Mapping\ToOneAssociationMetadata; |
8
|
|
|
use PDO; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* The ArrayHydrator produces a nested array "graph" that is often (not always) |
12
|
|
|
* interchangeable with the corresponding object graph for read-only access. |
13
|
|
|
*/ |
14
|
|
|
class ArrayHydrator extends AbstractHydrator |
15
|
|
|
{ |
16
|
|
|
/** |
17
|
|
|
* @var bool[] |
18
|
|
|
*/ |
19
|
|
|
private $rootAliases = []; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var bool |
23
|
|
|
*/ |
24
|
|
|
private $isSimpleQuery = false; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var mixed[][] |
28
|
|
|
*/ |
29
|
|
|
private $identifierMap = []; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var mixed[] |
33
|
|
|
*/ |
34
|
|
|
private $resultPointers = []; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var string[] |
38
|
|
|
*/ |
39
|
|
|
private $idTemplate = []; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var int |
43
|
|
|
*/ |
44
|
|
|
private $resultCounter = 0; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* {@inheritdoc} |
48
|
|
|
*/ |
49
|
84 |
|
protected function prepare() |
50
|
|
|
{ |
51
|
84 |
|
$simpleQuery = 0; |
52
|
|
|
|
53
|
84 |
|
foreach ($this->rsm->aliasMap as $dqlAlias => $className) { |
54
|
67 |
|
$this->identifierMap[$dqlAlias] = []; |
55
|
67 |
|
$this->resultPointers[$dqlAlias] = []; |
56
|
67 |
|
$this->idTemplate[$dqlAlias] = ''; |
57
|
67 |
|
$simpleQuery += 1; // avoiding counting the alias map |
58
|
|
|
} |
59
|
|
|
|
60
|
84 |
|
$this->isSimpleQuery = $simpleQuery < 2; |
61
|
84 |
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* {@inheritdoc} |
65
|
|
|
*/ |
66
|
81 |
|
protected function hydrateAllData() |
67
|
|
|
{ |
68
|
81 |
|
$result = []; |
69
|
|
|
|
70
|
81 |
|
while ($data = $this->stmt->fetch(PDO::FETCH_ASSOC)) { |
71
|
79 |
|
$this->hydrateRowData($data, $result); |
72
|
|
|
} |
73
|
|
|
|
74
|
81 |
|
return $result; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* {@inheritdoc} |
79
|
|
|
*/ |
80
|
82 |
|
protected function hydrateRowData(array $row, array &$result) |
81
|
|
|
{ |
82
|
|
|
// 1) Initialize |
83
|
82 |
|
$id = $this->idTemplate; // initialize the id-memory |
84
|
82 |
|
$nonemptyComponents = []; |
85
|
82 |
|
$rowData = $this->gatherRowData($row, $id, $nonemptyComponents); |
86
|
|
|
|
87
|
|
|
// 2) Now hydrate the data found in the current row. |
88
|
82 |
|
foreach ($rowData['data'] as $dqlAlias => $data) { |
89
|
66 |
|
$index = false; |
90
|
|
|
|
91
|
66 |
|
if (isset($this->rsm->parentAliasMap[$dqlAlias])) { |
92
|
|
|
// It's a joined result |
93
|
|
|
|
94
|
18 |
|
$parent = $this->rsm->parentAliasMap[$dqlAlias]; |
95
|
18 |
|
$path = $parent . '.' . $dqlAlias; |
96
|
|
|
|
97
|
|
|
// missing parent data, skipping as RIGHT JOIN hydration is not supported. |
98
|
18 |
|
if (! isset($nonemptyComponents[$parent])) { |
99
|
|
|
continue; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
// Get a reference to the right element in the result tree. |
103
|
|
|
// This element will get the associated element attached. |
104
|
18 |
|
if ($this->rsm->isMixed && isset($this->rootAliases[$parent])) { |
105
|
10 |
|
$first = reset($this->resultPointers); |
106
|
|
|
// TODO: Exception if $key === null ? |
107
|
10 |
|
$baseElement =& $this->resultPointers[$parent][key($first)]; |
108
|
9 |
|
} elseif (isset($this->resultPointers[$parent])) { |
109
|
9 |
|
$baseElement =& $this->resultPointers[$parent]; |
110
|
|
|
} else { |
111
|
|
|
unset($this->resultPointers[$dqlAlias]); // Ticket #1228 |
112
|
|
|
|
113
|
|
|
continue; |
114
|
|
|
} |
115
|
|
|
|
116
|
18 |
|
$relationAlias = $this->rsm->relationMap[$dqlAlias]; |
117
|
18 |
|
$parentClass = $this->metadataCache[$this->rsm->aliasMap[$parent]]; |
118
|
18 |
|
$relation = $parentClass->getProperty($relationAlias); |
119
|
|
|
|
120
|
|
|
// Check the type of the relation (many or single-valued) |
121
|
18 |
|
if (! $relation instanceof ToOneAssociationMetadata) { |
122
|
15 |
|
$oneToOne = false; |
123
|
|
|
|
124
|
15 |
|
if (! isset($baseElement[$relationAlias])) { |
125
|
15 |
|
$baseElement[$relationAlias] = []; |
126
|
|
|
} |
127
|
|
|
|
128
|
15 |
|
if (isset($nonemptyComponents[$dqlAlias])) { |
129
|
15 |
|
$indexExists = isset($this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); |
130
|
15 |
|
$index = $indexExists ? $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; |
131
|
15 |
|
$indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false; |
132
|
|
|
|
133
|
15 |
|
if (! $indexExists || ! $indexIsValid) { |
134
|
15 |
|
$element = $data; |
135
|
|
|
|
136
|
15 |
|
if (isset($this->rsm->indexByMap[$dqlAlias])) { |
137
|
5 |
|
$baseElement[$relationAlias][$row[$this->rsm->indexByMap[$dqlAlias]]] = $element; |
138
|
|
|
} else { |
139
|
10 |
|
$baseElement[$relationAlias][] = $element; |
140
|
|
|
} |
141
|
|
|
|
142
|
15 |
|
end($baseElement[$relationAlias]); |
143
|
|
|
|
144
|
15 |
|
$this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = key($baseElement[$relationAlias]); |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
} else { |
148
|
6 |
|
$oneToOne = true; |
149
|
|
|
|
150
|
6 |
|
if (( ! isset($nonemptyComponents[$dqlAlias])) && |
151
|
6 |
|
( ! isset($baseElement[$relationAlias])) |
152
|
|
|
) { |
153
|
|
|
$baseElement[$relationAlias] = null; |
154
|
6 |
|
} elseif (! isset($baseElement[$relationAlias])) { |
155
|
5 |
|
$baseElement[$relationAlias] = $data; |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
18 |
|
$coll =& $baseElement[$relationAlias]; |
160
|
|
|
|
161
|
18 |
|
if (is_array($coll)) { |
162
|
18 |
|
$this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne); |
163
|
|
|
} |
164
|
|
|
} else { |
165
|
|
|
// It's a root result element |
166
|
|
|
|
167
|
66 |
|
$this->rootAliases[$dqlAlias] = true; // Mark as root |
168
|
66 |
|
$entityKey = $this->rsm->entityMappings[$dqlAlias] ?: 0; |
169
|
|
|
|
170
|
|
|
// if this row has a NULL value for the root result id then make it a null result. |
171
|
66 |
|
if (! isset($nonemptyComponents[$dqlAlias])) { |
172
|
4 |
|
$result[] = $this->rsm->isMixed |
173
|
4 |
|
? [$entityKey => null] |
174
|
|
|
: null; |
175
|
|
|
|
176
|
4 |
|
$resultKey = $this->resultCounter; |
177
|
4 |
|
++$this->resultCounter; |
178
|
|
|
|
179
|
4 |
|
continue; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
// Check for an existing element |
183
|
66 |
|
if ($this->isSimpleQuery || ! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) { |
184
|
66 |
|
$element = $this->rsm->isMixed |
185
|
48 |
|
? [$entityKey => $data] |
186
|
66 |
|
: $data; |
187
|
|
|
|
188
|
66 |
|
if (isset($this->rsm->indexByMap[$dqlAlias])) { |
189
|
9 |
|
$resultKey = $row[$this->rsm->indexByMap[$dqlAlias]]; |
190
|
9 |
|
$result[$resultKey] = $element; |
191
|
|
|
} else { |
192
|
57 |
|
$resultKey = $this->resultCounter; |
193
|
57 |
|
$result[] = $element; |
194
|
|
|
|
195
|
57 |
|
++$this->resultCounter; |
196
|
|
|
} |
197
|
|
|
|
198
|
66 |
|
$this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; |
199
|
|
|
} else { |
200
|
15 |
|
$index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]]; |
201
|
15 |
|
$resultKey = $index; |
202
|
|
|
} |
203
|
|
|
|
204
|
66 |
|
$this->updateResultPointer($result, $index, $dqlAlias, false); |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
208
|
82 |
|
if (! isset($resultKey)) { |
209
|
16 |
|
$this->resultCounter++; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
// Append scalar values to mixed result sets |
213
|
82 |
|
if (isset($rowData['scalars'])) { |
214
|
57 |
|
if (! isset($resultKey)) { |
215
|
|
|
// this only ever happens when no object is fetched (scalar result only) |
216
|
14 |
|
$resultKey = isset($this->rsm->indexByMap['scalars']) |
217
|
|
|
? $row[$this->rsm->indexByMap['scalars']] |
218
|
14 |
|
: $this->resultCounter - 1; |
219
|
|
|
} |
220
|
|
|
|
221
|
57 |
|
foreach ($rowData['scalars'] as $name => $value) { |
222
|
57 |
|
$result[$resultKey][$name] = $value; |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
// Append new object to mixed result sets |
227
|
82 |
|
if (isset($rowData['newObjects'])) { |
228
|
2 |
|
if (! isset($resultKey)) { |
229
|
2 |
|
$resultKey = $this->resultCounter - 1; |
230
|
|
|
} |
231
|
|
|
|
232
|
2 |
|
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0); |
233
|
2 |
|
$onlyOneRootAlias = $scalarCount === 0 && count($rowData['newObjects']) === 1; |
234
|
|
|
|
235
|
2 |
|
foreach ($rowData['newObjects'] as $objIndex => $newObject) { |
236
|
2 |
|
$class = $newObject['class']; |
237
|
2 |
|
$args = $newObject['args']; |
238
|
2 |
|
$obj = $class->newInstanceArgs($args); |
239
|
|
|
|
240
|
2 |
|
if ($onlyOneRootAlias || \count($args) === $scalarCount) { |
241
|
2 |
|
$result[$resultKey] = $obj; |
242
|
|
|
|
243
|
2 |
|
continue; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
$result[$resultKey][$objIndex] = $obj; |
247
|
|
|
} |
248
|
|
|
} |
249
|
82 |
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Updates the result pointer for an Entity. The result pointers point to the |
253
|
|
|
* last seen instance of each Entity type. This is used for graph construction. |
254
|
|
|
* |
255
|
|
|
* @param mixed[] $coll The element. |
256
|
|
|
* @param bool|int $index Index of the element in the collection. |
257
|
|
|
* @param string $dqlAlias |
258
|
|
|
* @param bool $oneToOne Whether it is a single-valued association or not. |
259
|
|
|
* |
260
|
|
|
*/ |
261
|
66 |
|
private function updateResultPointer(array &$coll, $index, $dqlAlias, $oneToOne) |
262
|
|
|
{ |
263
|
66 |
|
if ($oneToOne) { |
264
|
5 |
|
$this->resultPointers[$dqlAlias] =& $coll; |
265
|
|
|
|
266
|
5 |
|
return; |
267
|
|
|
} |
268
|
|
|
|
269
|
66 |
|
if ($index !== false) { |
270
|
15 |
|
$this->resultPointers[$dqlAlias] =& $coll[$index]; |
271
|
|
|
|
272
|
15 |
|
return; |
273
|
|
|
} |
274
|
|
|
|
275
|
66 |
|
if (! $coll) { |
276
|
1 |
|
return; |
277
|
|
|
} |
278
|
|
|
|
279
|
66 |
|
end($coll); |
280
|
66 |
|
$this->resultPointers[$dqlAlias] =& $coll[key($coll)]; |
281
|
|
|
|
282
|
66 |
|
return; |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
|