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 ($coll === null) { |
264
|
|
|
unset($this->resultPointers[$dqlAlias]); // Ticket #1228 |
265
|
|
|
|
266
|
|
|
return; |
267
|
|
|
} |
268
|
|
|
|
269
|
66 |
|
if ($oneToOne) { |
270
|
5 |
|
$this->resultPointers[$dqlAlias] =& $coll; |
271
|
|
|
|
272
|
5 |
|
return; |
273
|
|
|
} |
274
|
|
|
|
275
|
66 |
|
if ($index !== false) { |
276
|
15 |
|
$this->resultPointers[$dqlAlias] =& $coll[$index]; |
277
|
|
|
|
278
|
15 |
|
return; |
279
|
|
|
} |
280
|
|
|
|
281
|
66 |
|
if (! $coll) { |
|
|
|
|
282
|
1 |
|
return; |
283
|
|
|
} |
284
|
|
|
|
285
|
66 |
|
end($coll); |
286
|
66 |
|
$this->resultPointers[$dqlAlias] =& $coll[key($coll)]; |
287
|
|
|
|
288
|
66 |
|
return; |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.