|
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 LGPL. For more information, see |
|
17 * <http://www.doctrine-project.org>. |
|
18 */ |
|
19 |
|
20 namespace Doctrine\ORM\Internal\Hydration; |
|
21 |
|
22 use PDO, |
|
23 Doctrine\ORM\Mapping\ClassMetadata, |
|
24 Doctrine\ORM\PersistentCollection, |
|
25 Doctrine\ORM\Query, |
|
26 Doctrine\Common\Collections\ArrayCollection, |
|
27 Doctrine\Common\Collections\Collection; |
|
28 |
|
29 /** |
|
30 * The ObjectHydrator constructs an object graph out of an SQL result set. |
|
31 * |
|
32 * @author Roman Borschel <roman@code-factory.org> |
|
33 * @since 2.0 |
|
34 * @internal Highly performance-sensitive code. |
|
35 */ |
|
36 class ObjectHydrator extends AbstractHydrator |
|
37 { |
|
38 /* Local ClassMetadata cache to avoid going to the EntityManager all the time. |
|
39 * This local cache is maintained between hydration runs and not cleared. |
|
40 */ |
|
41 private $_ce = array(); |
|
42 |
|
43 /* The following parts are reinitialized on every hydration run. */ |
|
44 |
|
45 private $_identifierMap; |
|
46 private $_resultPointers; |
|
47 private $_idTemplate; |
|
48 private $_resultCounter; |
|
49 private $_rootAliases = array(); |
|
50 private $_initializedCollections = array(); |
|
51 private $_existingCollections = array(); |
|
52 //private $_createdEntities; |
|
53 |
|
54 |
|
55 /** @override */ |
|
56 protected function _prepare() |
|
57 { |
|
58 $this->_identifierMap = |
|
59 $this->_resultPointers = |
|
60 $this->_idTemplate = array(); |
|
61 $this->_resultCounter = 0; |
|
62 if (!isset($this->_hints['deferEagerLoad'])) { |
|
63 $this->_hints['deferEagerLoad'] = true; |
|
64 } |
|
65 |
|
66 foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { |
|
67 $this->_identifierMap[$dqlAlias] = array(); |
|
68 $this->_idTemplate[$dqlAlias] = ''; |
|
69 $class = $this->_em->getClassMetadata($className); |
|
70 |
|
71 if ( ! isset($this->_ce[$className])) { |
|
72 $this->_ce[$className] = $class; |
|
73 } |
|
74 |
|
75 // Remember which associations are "fetch joined", so that we know where to inject |
|
76 // collection stubs or proxies and where not. |
|
77 if (isset($this->_rsm->relationMap[$dqlAlias])) { |
|
78 if ( ! isset($this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]])) { |
|
79 throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $this->_rsm->parentAliasMap[$dqlAlias]); |
|
80 } |
|
81 |
|
82 $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]]; |
|
83 $sourceClass = $this->_getClassMetadata($sourceClassName); |
|
84 $assoc = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]]; |
|
85 $this->_hints['fetched'][$sourceClassName][$assoc['fieldName']] = true; |
|
86 if ($sourceClass->subClasses) { |
|
87 foreach ($sourceClass->subClasses as $sourceSubclassName) { |
|
88 $this->_hints['fetched'][$sourceSubclassName][$assoc['fieldName']] = true; |
|
89 } |
|
90 } |
|
91 if ($assoc['type'] != ClassMetadata::MANY_TO_MANY) { |
|
92 // Mark any non-collection opposite sides as fetched, too. |
|
93 if ($assoc['mappedBy']) { |
|
94 $this->_hints['fetched'][$className][$assoc['mappedBy']] = true; |
|
95 } else { |
|
96 if ($assoc['inversedBy']) { |
|
97 $inverseAssoc = $class->associationMappings[$assoc['inversedBy']]; |
|
98 if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) { |
|
99 $this->_hints['fetched'][$className][$inverseAssoc['fieldName']] = true; |
|
100 if ($class->subClasses) { |
|
101 foreach ($class->subClasses as $targetSubclassName) { |
|
102 $this->_hints['fetched'][$targetSubclassName][$inverseAssoc['fieldName']] = true; |
|
103 } |
|
104 } |
|
105 } |
|
106 } |
|
107 } |
|
108 } |
|
109 } |
|
110 } |
|
111 } |
|
112 |
|
113 /** |
|
114 * {@inheritdoc} |
|
115 */ |
|
116 protected function _cleanup() |
|
117 { |
|
118 $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true; |
|
119 |
|
120 parent::_cleanup(); |
|
121 $this->_identifierMap = |
|
122 $this->_initializedCollections = |
|
123 $this->_existingCollections = |
|
124 $this->_resultPointers = array(); |
|
125 |
|
126 if ($eagerLoad) { |
|
127 $this->_em->getUnitOfWork()->triggerEagerLoads(); |
|
128 } |
|
129 } |
|
130 |
|
131 /** |
|
132 * {@inheritdoc} |
|
133 */ |
|
134 protected function _hydrateAll() |
|
135 { |
|
136 $result = array(); |
|
137 $cache = array(); |
|
138 |
|
139 while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { |
|
140 $this->_hydrateRow($row, $cache, $result); |
|
141 } |
|
142 |
|
143 // Take snapshots from all newly initialized collections |
|
144 foreach ($this->_initializedCollections as $coll) { |
|
145 $coll->takeSnapshot(); |
|
146 } |
|
147 |
|
148 return $result; |
|
149 } |
|
150 |
|
151 /** |
|
152 * Initializes a related collection. |
|
153 * |
|
154 * @param object $entity The entity to which the collection belongs. |
|
155 * @param string $name The name of the field on the entity that holds the collection. |
|
156 */ |
|
157 private function _initRelatedCollection($entity, $class, $fieldName) |
|
158 { |
|
159 $oid = spl_object_hash($entity); |
|
160 $relation = $class->associationMappings[$fieldName]; |
|
161 |
|
162 $value = $class->reflFields[$fieldName]->getValue($entity); |
|
163 if ($value === null) { |
|
164 $value = new ArrayCollection; |
|
165 } |
|
166 |
|
167 if ( ! $value instanceof PersistentCollection) { |
|
168 $value = new PersistentCollection( |
|
169 $this->_em, |
|
170 $this->_ce[$relation['targetEntity']], |
|
171 $value |
|
172 ); |
|
173 $value->setOwner($entity, $relation); |
|
174 $class->reflFields[$fieldName]->setValue($entity, $value); |
|
175 $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value); |
|
176 $this->_initializedCollections[$oid . $fieldName] = $value; |
|
177 } else if (isset($this->_hints[Query::HINT_REFRESH]) || |
|
178 isset($this->_hints['fetched'][$class->name][$fieldName]) && |
|
179 ! $value->isInitialized()) { |
|
180 // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED! |
|
181 $value->setDirty(false); |
|
182 $value->setInitialized(true); |
|
183 $value->unwrap()->clear(); |
|
184 $this->_initializedCollections[$oid . $fieldName] = $value; |
|
185 } else { |
|
186 // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN! |
|
187 $this->_existingCollections[$oid . $fieldName] = $value; |
|
188 } |
|
189 |
|
190 return $value; |
|
191 } |
|
192 |
|
193 /** |
|
194 * Gets an entity instance. |
|
195 * |
|
196 * @param $data The instance data. |
|
197 * @param $dqlAlias The DQL alias of the entity's class. |
|
198 * @return object The entity. |
|
199 */ |
|
200 private function _getEntity(array $data, $dqlAlias) |
|
201 { |
|
202 $className = $this->_rsm->aliasMap[$dqlAlias]; |
|
203 if (isset($this->_rsm->discriminatorColumns[$dqlAlias])) { |
|
204 $discrColumn = $this->_rsm->metaMappings[$this->_rsm->discriminatorColumns[$dqlAlias]]; |
|
205 $className = $this->_ce[$className]->discriminatorMap[$data[$discrColumn]]; |
|
206 unset($data[$discrColumn]); |
|
207 } |
|
208 |
|
209 if (isset($this->_hints[Query::HINT_REFRESH_ENTITY]) && isset($this->_rootAliases[$dqlAlias])) { |
|
210 $class = $this->_ce[$className]; |
|
211 $this->registerManaged($class, $this->_hints[Query::HINT_REFRESH_ENTITY], $data); |
|
212 } |
|
213 |
|
214 return $this->_uow->createEntity($className, $data, $this->_hints); |
|
215 } |
|
216 |
|
217 private function _getEntityFromIdentityMap($className, array $data) |
|
218 { |
|
219 // TODO: Abstract this code and UnitOfWork::createEntity() equivalent? |
|
220 $class = $this->_ce[$className]; |
|
221 /* @var $class ClassMetadata */ |
|
222 if ($class->isIdentifierComposite) { |
|
223 $idHash = ''; |
|
224 foreach ($class->identifier as $fieldName) { |
|
225 if (isset($class->associationMappings[$fieldName])) { |
|
226 $idHash .= $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']] . ' '; |
|
227 } else { |
|
228 $idHash .= $data[$fieldName] . ' '; |
|
229 } |
|
230 } |
|
231 return $this->_uow->tryGetByIdHash(rtrim($idHash), $class->rootEntityName); |
|
232 } else if (isset($class->associationMappings[$class->identifier[0]])) { |
|
233 return $this->_uow->tryGetByIdHash($data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']], $class->rootEntityName); |
|
234 } else { |
|
235 return $this->_uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName); |
|
236 } |
|
237 } |
|
238 |
|
239 /** |
|
240 * Gets a ClassMetadata instance from the local cache. |
|
241 * If the instance is not yet in the local cache, it is loaded into the |
|
242 * local cache. |
|
243 * |
|
244 * @param string $className The name of the class. |
|
245 * @return ClassMetadata |
|
246 */ |
|
247 private function _getClassMetadata($className) |
|
248 { |
|
249 if ( ! isset($this->_ce[$className])) { |
|
250 $this->_ce[$className] = $this->_em->getClassMetadata($className); |
|
251 } |
|
252 return $this->_ce[$className]; |
|
253 } |
|
254 |
|
255 /** |
|
256 * Hydrates a single row in an SQL result set. |
|
257 * |
|
258 * @internal |
|
259 * First, the data of the row is split into chunks where each chunk contains data |
|
260 * that belongs to a particular component/class. Afterwards, all these chunks |
|
261 * are processed, one after the other. For each chunk of class data only one of the |
|
262 * following code paths is executed: |
|
263 * |
|
264 * Path A: The data chunk belongs to a joined/associated object and the association |
|
265 * is collection-valued. |
|
266 * Path B: The data chunk belongs to a joined/associated object and the association |
|
267 * is single-valued. |
|
268 * Path C: The data chunk belongs to a root result element/object that appears in the topmost |
|
269 * level of the hydrated result. A typical example are the objects of the type |
|
270 * specified by the FROM clause in a DQL query. |
|
271 * |
|
272 * @param array $data The data of the row to process. |
|
273 * @param array $cache The cache to use. |
|
274 * @param array $result The result array to fill. |
|
275 */ |
|
276 protected function _hydrateRow(array $data, array &$cache, array &$result) |
|
277 { |
|
278 // Initialize |
|
279 $id = $this->_idTemplate; // initialize the id-memory |
|
280 $nonemptyComponents = array(); |
|
281 // Split the row data into chunks of class data. |
|
282 $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents); |
|
283 |
|
284 // Extract scalar values. They're appended at the end. |
|
285 if (isset($rowData['scalars'])) { |
|
286 $scalars = $rowData['scalars']; |
|
287 unset($rowData['scalars']); |
|
288 if (empty($rowData)) { |
|
289 ++$this->_resultCounter; |
|
290 } |
|
291 } |
|
292 |
|
293 // Hydrate the data chunks |
|
294 foreach ($rowData as $dqlAlias => $data) { |
|
295 $entityName = $this->_rsm->aliasMap[$dqlAlias]; |
|
296 |
|
297 if (isset($this->_rsm->parentAliasMap[$dqlAlias])) { |
|
298 // It's a joined result |
|
299 |
|
300 $parentAlias = $this->_rsm->parentAliasMap[$dqlAlias]; |
|
301 // we need the $path to save into the identifier map which entities were already |
|
302 // seen for this parent-child relationship |
|
303 $path = $parentAlias . '.' . $dqlAlias; |
|
304 |
|
305 // Get a reference to the parent object to which the joined element belongs. |
|
306 if ($this->_rsm->isMixed && isset($this->_rootAliases[$parentAlias])) { |
|
307 $first = reset($this->_resultPointers); |
|
308 $parentObject = $this->_resultPointers[$parentAlias][key($first)]; |
|
309 } else if (isset($this->_resultPointers[$parentAlias])) { |
|
310 $parentObject = $this->_resultPointers[$parentAlias]; |
|
311 } else { |
|
312 // Parent object of relation not found, so skip it. |
|
313 continue; |
|
314 } |
|
315 |
|
316 $parentClass = $this->_ce[$this->_rsm->aliasMap[$parentAlias]]; |
|
317 $oid = spl_object_hash($parentObject); |
|
318 $relationField = $this->_rsm->relationMap[$dqlAlias]; |
|
319 $relation = $parentClass->associationMappings[$relationField]; |
|
320 $reflField = $parentClass->reflFields[$relationField]; |
|
321 |
|
322 // Check the type of the relation (many or single-valued) |
|
323 if ( ! ($relation['type'] & ClassMetadata::TO_ONE)) { |
|
324 // PATH A: Collection-valued association |
|
325 if (isset($nonemptyComponents[$dqlAlias])) { |
|
326 $collKey = $oid . $relationField; |
|
327 if (isset($this->_initializedCollections[$collKey])) { |
|
328 $reflFieldValue = $this->_initializedCollections[$collKey]; |
|
329 } else if ( ! isset($this->_existingCollections[$collKey])) { |
|
330 $reflFieldValue = $this->_initRelatedCollection($parentObject, $parentClass, $relationField); |
|
331 } |
|
332 |
|
333 $indexExists = isset($this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); |
|
334 $index = $indexExists ? $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false; |
|
335 $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false; |
|
336 |
|
337 if ( ! $indexExists || ! $indexIsValid) { |
|
338 if (isset($this->_existingCollections[$collKey])) { |
|
339 // Collection exists, only look for the element in the identity map. |
|
340 if ($element = $this->_getEntityFromIdentityMap($entityName, $data)) { |
|
341 $this->_resultPointers[$dqlAlias] = $element; |
|
342 } else { |
|
343 unset($this->_resultPointers[$dqlAlias]); |
|
344 } |
|
345 } else { |
|
346 $element = $this->_getEntity($data, $dqlAlias); |
|
347 |
|
348 if (isset($this->_rsm->indexByMap[$dqlAlias])) { |
|
349 $field = $this->_rsm->indexByMap[$dqlAlias]; |
|
350 $indexValue = $this->_ce[$entityName]->reflFields[$field]->getValue($element); |
|
351 $reflFieldValue->hydrateSet($indexValue, $element); |
|
352 $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; |
|
353 } else { |
|
354 $reflFieldValue->hydrateAdd($element); |
|
355 $reflFieldValue->last(); |
|
356 $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key(); |
|
357 } |
|
358 // Update result pointer |
|
359 $this->_resultPointers[$dqlAlias] = $element; |
|
360 } |
|
361 } else { |
|
362 // Update result pointer |
|
363 $this->_resultPointers[$dqlAlias] = $reflFieldValue[$index]; |
|
364 } |
|
365 } else if ( ! $reflField->getValue($parentObject)) { |
|
366 $coll = new PersistentCollection($this->_em, $this->_ce[$entityName], new ArrayCollection); |
|
367 $coll->setOwner($parentObject, $relation); |
|
368 $reflField->setValue($parentObject, $coll); |
|
369 $this->_uow->setOriginalEntityProperty($oid, $relationField, $coll); |
|
370 } |
|
371 } else { |
|
372 // PATH B: Single-valued association |
|
373 $reflFieldValue = $reflField->getValue($parentObject); |
|
374 if ( ! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH])) { |
|
375 if (isset($nonemptyComponents[$dqlAlias])) { |
|
376 $element = $this->_getEntity($data, $dqlAlias); |
|
377 $reflField->setValue($parentObject, $element); |
|
378 $this->_uow->setOriginalEntityProperty($oid, $relationField, $element); |
|
379 $targetClass = $this->_ce[$relation['targetEntity']]; |
|
380 if ($relation['isOwningSide']) { |
|
381 //TODO: Just check hints['fetched'] here? |
|
382 // If there is an inverse mapping on the target class its bidirectional |
|
383 if ($relation['inversedBy']) { |
|
384 $inverseAssoc = $targetClass->associationMappings[$relation['inversedBy']]; |
|
385 if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) { |
|
386 $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject); |
|
387 $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $inverseAssoc['fieldName'], $parentObject); |
|
388 } |
|
389 } else if ($parentClass === $targetClass && $relation['mappedBy']) { |
|
390 // Special case: bi-directional self-referencing one-one on the same class |
|
391 $targetClass->reflFields[$relationField]->setValue($element, $parentObject); |
|
392 } |
|
393 } else { |
|
394 // For sure bidirectional, as there is no inverse side in unidirectional mappings |
|
395 $targetClass->reflFields[$relation['mappedBy']]->setValue($element, $parentObject); |
|
396 $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $relation['mappedBy'], $parentObject); |
|
397 } |
|
398 // Update result pointer |
|
399 $this->_resultPointers[$dqlAlias] = $element; |
|
400 } |
|
401 // else leave $reflFieldValue null for single-valued associations |
|
402 } else { |
|
403 // Update result pointer |
|
404 $this->_resultPointers[$dqlAlias] = $reflFieldValue; |
|
405 } |
|
406 } |
|
407 } else { |
|
408 // PATH C: Its a root result element |
|
409 $this->_rootAliases[$dqlAlias] = true; // Mark as root alias |
|
410 |
|
411 if ( ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { |
|
412 $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias); |
|
413 if (isset($this->_rsm->indexByMap[$dqlAlias])) { |
|
414 $field = $this->_rsm->indexByMap[$dqlAlias]; |
|
415 $key = $this->_ce[$entityName]->reflFields[$field]->getValue($element); |
|
416 if ($this->_rsm->isMixed) { |
|
417 $element = array($key => $element); |
|
418 $result[] = $element; |
|
419 $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; |
|
420 ++$this->_resultCounter; |
|
421 } else { |
|
422 $result[$key] = $element; |
|
423 $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $key; |
|
424 } |
|
425 |
|
426 if (isset($this->_hints['collection'])) { |
|
427 $this->_hints['collection']->hydrateSet($key, $element); |
|
428 } |
|
429 } else { |
|
430 if ($this->_rsm->isMixed) { |
|
431 $element = array(0 => $element); |
|
432 } |
|
433 $result[] = $element; |
|
434 $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; |
|
435 ++$this->_resultCounter; |
|
436 |
|
437 if (isset($this->_hints['collection'])) { |
|
438 $this->_hints['collection']->hydrateAdd($element); |
|
439 } |
|
440 } |
|
441 |
|
442 // Update result pointer |
|
443 $this->_resultPointers[$dqlAlias] = $element; |
|
444 |
|
445 } else { |
|
446 // Update result pointer |
|
447 $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]]; |
|
448 $this->_resultPointers[$dqlAlias] = $result[$index]; |
|
449 /*if ($this->_rsm->isMixed) { |
|
450 $result[] = $result[$index]; |
|
451 ++$this->_resultCounter; |
|
452 }*/ |
|
453 } |
|
454 } |
|
455 } |
|
456 |
|
457 // Append scalar values to mixed result sets |
|
458 if (isset($scalars)) { |
|
459 foreach ($scalars as $name => $value) { |
|
460 $result[$this->_resultCounter - 1][$name] = $value; |
|
461 } |
|
462 } |
|
463 } |
|
464 } |