vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php line 124

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM\Internal\Hydration;
  4. use Doctrine\Common\Collections\ArrayCollection;
  5. use Doctrine\Common\Proxy\Proxy;
  6. use Doctrine\ORM\Mapping\ClassMetadata;
  7. use Doctrine\ORM\PersistentCollection;
  8. use Doctrine\ORM\Query;
  9. use Doctrine\ORM\UnitOfWork;
  10. use function array_fill_keys;
  11. use function array_keys;
  12. use function count;
  13. use function is_array;
  14. use function key;
  15. use function ltrim;
  16. use function spl_object_id;
  17. /**
  18. * The ObjectHydrator constructs an object graph out of an SQL result set.
  19. *
  20. * Internal note: Highly performance-sensitive code.
  21. */
  22. class ObjectHydrator extends AbstractHydrator
  23. {
  24. /** @var mixed[] */
  25. private $identifierMap = [];
  26. /** @var mixed[] */
  27. private $resultPointers = [];
  28. /** @var mixed[] */
  29. private $idTemplate = [];
  30. /** @var int */
  31. private $resultCounter = 0;
  32. /** @var mixed[] */
  33. private $rootAliases = [];
  34. /** @var mixed[] */
  35. private $initializedCollections = [];
  36. /** @var mixed[] */
  37. private $existingCollections = [];
  38. /**
  39. * {@inheritdoc}
  40. */
  41. protected function prepare()
  42. {
  43. if (! isset($this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD])) {
  44. $this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD] = true;
  45. }
  46. foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) {
  47. $this->identifierMap[$dqlAlias] = [];
  48. $this->idTemplate[$dqlAlias] = '';
  49. // Remember which associations are "fetch joined", so that we know where to inject
  50. // collection stubs or proxies and where not.
  51. if (! isset($this->resultSetMapping()->relationMap[$dqlAlias])) {
  52. continue;
  53. }
  54. $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
  55. if (! isset($this->resultSetMapping()->aliasMap[$parent])) {
  56. throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent);
  57. }
  58. $sourceClassName = $this->resultSetMapping()->aliasMap[$parent];
  59. $sourceClass = $this->getClassMetadata($sourceClassName);
  60. $assoc = $sourceClass->associationMappings[$this->resultSetMapping()->relationMap[$dqlAlias]];
  61. $this->_hints['fetched'][$parent][$assoc['fieldName']] = true;
  62. if ($assoc['type'] === ClassMetadata::MANY_TO_MANY) {
  63. continue;
  64. }
  65. // Mark any non-collection opposite sides as fetched, too.
  66. if ($assoc['mappedBy']) {
  67. $this->_hints['fetched'][$dqlAlias][$assoc['mappedBy']] = true;
  68. continue;
  69. }
  70. // handle fetch-joined owning side bi-directional one-to-one associations
  71. if ($assoc['inversedBy']) {
  72. $class = $this->getClassMetadata($className);
  73. $inverseAssoc = $class->associationMappings[$assoc['inversedBy']];
  74. if (! ($inverseAssoc['type'] & ClassMetadata::TO_ONE)) {
  75. continue;
  76. }
  77. $this->_hints['fetched'][$dqlAlias][$inverseAssoc['fieldName']] = true;
  78. }
  79. }
  80. }
  81. /**
  82. * {@inheritdoc}
  83. */
  84. protected function cleanup()
  85. {
  86. $eagerLoad = isset($this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD]) && $this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD] === true;
  87. parent::cleanup();
  88. $this->identifierMap =
  89. $this->initializedCollections =
  90. $this->existingCollections =
  91. $this->resultPointers = [];
  92. if ($eagerLoad) {
  93. $this->_uow->triggerEagerLoads();
  94. }
  95. $this->_uow->hydrationComplete();
  96. }
  97. protected function cleanupAfterRowIteration(): void
  98. {
  99. $this->identifierMap =
  100. $this->initializedCollections =
  101. $this->existingCollections =
  102. $this->resultPointers = [];
  103. }
  104. /**
  105. * {@inheritdoc}
  106. */
  107. protected function hydrateAllData()
  108. {
  109. $result = [];
  110. while ($row = $this->statement()->fetchAssociative()) {
  111. $this->hydrateRowData($row, $result);
  112. }
  113. // Take snapshots from all newly initialized collections
  114. foreach ($this->initializedCollections as $coll) {
  115. $coll->takeSnapshot();
  116. }
  117. return $result;
  118. }
  119. /**
  120. * Initializes a related collection.
  121. *
  122. * @param object $entity The entity to which the collection belongs.
  123. * @param string $fieldName The name of the field on the entity that holds the collection.
  124. * @param string $parentDqlAlias Alias of the parent fetch joining this collection.
  125. */
  126. private function initRelatedCollection(
  127. $entity,
  128. ClassMetadata $class,
  129. string $fieldName,
  130. string $parentDqlAlias
  131. ): PersistentCollection {
  132. $oid = spl_object_id($entity);
  133. $relation = $class->associationMappings[$fieldName];
  134. $value = $class->reflFields[$fieldName]->getValue($entity);
  135. if ($value === null || is_array($value)) {
  136. $value = new ArrayCollection((array) $value);
  137. }
  138. if (! $value instanceof PersistentCollection) {
  139. $value = new PersistentCollection(
  140. $this->_em,
  141. $this->_metadataCache[$relation['targetEntity']],
  142. $value
  143. );
  144. $value->setOwner($entity, $relation);
  145. $class->reflFields[$fieldName]->setValue($entity, $value);
  146. $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value);
  147. $this->initializedCollections[$oid . $fieldName] = $value;
  148. } elseif (
  149. isset($this->_hints[Query::HINT_REFRESH]) ||
  150. isset($this->_hints['fetched'][$parentDqlAlias][$fieldName]) &&
  151. ! $value->isInitialized()
  152. ) {
  153. // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED!
  154. $value->setDirty(false);
  155. $value->setInitialized(true);
  156. $value->unwrap()->clear();
  157. $this->initializedCollections[$oid . $fieldName] = $value;
  158. } else {
  159. // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN!
  160. $this->existingCollections[$oid . $fieldName] = $value;
  161. }
  162. return $value;
  163. }
  164. /**
  165. * Gets an entity instance.
  166. *
  167. * @param string $dqlAlias The DQL alias of the entity's class.
  168. * @psalm-param array<string, mixed> $data The instance data.
  169. *
  170. * @return object
  171. *
  172. * @throws HydrationException
  173. */
  174. private function getEntity(array $data, string $dqlAlias)
  175. {
  176. $className = $this->resultSetMapping()->aliasMap[$dqlAlias];
  177. if (isset($this->resultSetMapping()->discriminatorColumns[$dqlAlias])) {
  178. $fieldName = $this->resultSetMapping()->discriminatorColumns[$dqlAlias];
  179. if (! isset($this->resultSetMapping()->metaMappings[$fieldName])) {
  180. throw HydrationException::missingDiscriminatorMetaMappingColumn($className, $fieldName, $dqlAlias);
  181. }
  182. $discrColumn = $this->resultSetMapping()->metaMappings[$fieldName];
  183. if (! isset($data[$discrColumn])) {
  184. throw HydrationException::missingDiscriminatorColumn($className, $discrColumn, $dqlAlias);
  185. }
  186. if ($data[$discrColumn] === '') {
  187. throw HydrationException::emptyDiscriminatorValue($dqlAlias);
  188. }
  189. $discrMap = $this->_metadataCache[$className]->discriminatorMap;
  190. $discriminatorValue = (string) $data[$discrColumn];
  191. if (! isset($discrMap[$discriminatorValue])) {
  192. throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap));
  193. }
  194. $className = $discrMap[$discriminatorValue];
  195. unset($data[$discrColumn]);
  196. }
  197. if (isset($this->_hints[Query::HINT_REFRESH_ENTITY], $this->rootAliases[$dqlAlias])) {
  198. $this->registerManaged($this->_metadataCache[$className], $this->_hints[Query::HINT_REFRESH_ENTITY], $data);
  199. }
  200. $this->_hints['fetchAlias'] = $dqlAlias;
  201. return $this->_uow->createEntity($className, $data, $this->_hints);
  202. }
  203. /**
  204. * @psalm-param class-string $className
  205. * @psalm-param array<string, mixed> $data
  206. *
  207. * @return mixed
  208. */
  209. private function getEntityFromIdentityMap(string $className, array $data)
  210. {
  211. // TODO: Abstract this code and UnitOfWork::createEntity() equivalent?
  212. $class = $this->_metadataCache[$className];
  213. if ($class->isIdentifierComposite) {
  214. $idHash = '';
  215. foreach ($class->identifier as $fieldName) {
  216. $idHash .= ' ' . (isset($class->associationMappings[$fieldName])
  217. ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
  218. : $data[$fieldName]);
  219. }
  220. return $this->_uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName);
  221. } elseif (isset($class->associationMappings[$class->identifier[0]])) {
  222. return $this->_uow->tryGetByIdHash($data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']], $class->rootEntityName);
  223. }
  224. return $this->_uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName);
  225. }
  226. /**
  227. * Hydrates a single row in an SQL result set.
  228. *
  229. * @internal
  230. * First, the data of the row is split into chunks where each chunk contains data
  231. * that belongs to a particular component/class. Afterwards, all these chunks
  232. * are processed, one after the other. For each chunk of class data only one of the
  233. * following code paths is executed:
  234. *
  235. * Path A: The data chunk belongs to a joined/associated object and the association
  236. * is collection-valued.
  237. * Path B: The data chunk belongs to a joined/associated object and the association
  238. * is single-valued.
  239. * Path C: The data chunk belongs to a root result element/object that appears in the topmost
  240. * level of the hydrated result. A typical example are the objects of the type
  241. * specified by the FROM clause in a DQL query.
  242. *
  243. * @param mixed[] $row The data of the row to process.
  244. * @param mixed[] $result The result array to fill.
  245. *
  246. * @return void
  247. */
  248. protected function hydrateRowData(array $row, array &$result)
  249. {
  250. // Initialize
  251. $id = $this->idTemplate; // initialize the id-memory
  252. $nonemptyComponents = [];
  253. // Split the row data into chunks of class data.
  254. $rowData = $this->gatherRowData($row, $id, $nonemptyComponents);
  255. // reset result pointers for each data row
  256. $this->resultPointers = [];
  257. // Hydrate the data chunks
  258. foreach ($rowData['data'] as $dqlAlias => $data) {
  259. $entityName = $this->resultSetMapping()->aliasMap[$dqlAlias];
  260. if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) {
  261. // It's a joined result
  262. $parentAlias = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
  263. // we need the $path to save into the identifier map which entities were already
  264. // seen for this parent-child relationship
  265. $path = $parentAlias . '.' . $dqlAlias;
  266. // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs
  267. if (! isset($nonemptyComponents[$parentAlias])) {
  268. // TODO: Add special case code where we hydrate the right join objects into identity map at least
  269. continue;
  270. }
  271. $parentClass = $this->_metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]];
  272. $relationField = $this->resultSetMapping()->relationMap[$dqlAlias];
  273. $relation = $parentClass->associationMappings[$relationField];
  274. $reflField = $parentClass->reflFields[$relationField];
  275. // Get a reference to the parent object to which the joined element belongs.
  276. if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) {
  277. $objectClass = $this->resultPointers[$parentAlias];
  278. $parentObject = $objectClass[key($objectClass)];
  279. } elseif (isset($this->resultPointers[$parentAlias])) {
  280. $parentObject = $this->resultPointers[$parentAlias];
  281. } else {
  282. // Parent object of relation not found, mark as not-fetched again
  283. $element = $this->getEntity($data, $dqlAlias);
  284. // Update result pointer and provide initial fetch data for parent
  285. $this->resultPointers[$dqlAlias] = $element;
  286. $rowData['data'][$parentAlias][$relationField] = $element;
  287. // Mark as not-fetched again
  288. unset($this->_hints['fetched'][$parentAlias][$relationField]);
  289. continue;
  290. }
  291. $oid = spl_object_id($parentObject);
  292. // Check the type of the relation (many or single-valued)
  293. if (! ($relation['type'] & ClassMetadata::TO_ONE)) {
  294. // PATH A: Collection-valued association
  295. $reflFieldValue = $reflField->getValue($parentObject);
  296. if (isset($nonemptyComponents[$dqlAlias])) {
  297. $collKey = $oid . $relationField;
  298. if (isset($this->initializedCollections[$collKey])) {
  299. $reflFieldValue = $this->initializedCollections[$collKey];
  300. } elseif (! isset($this->existingCollections[$collKey])) {
  301. $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
  302. }
  303. $indexExists = isset($this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]);
  304. $index = $indexExists ? $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false;
  305. $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false;
  306. if (! $indexExists || ! $indexIsValid) {
  307. if (isset($this->existingCollections[$collKey])) {
  308. // Collection exists, only look for the element in the identity map.
  309. $element = $this->getEntityFromIdentityMap($entityName, $data);
  310. if ($element) {
  311. $this->resultPointers[$dqlAlias] = $element;
  312. } else {
  313. unset($this->resultPointers[$dqlAlias]);
  314. }
  315. } else {
  316. $element = $this->getEntity($data, $dqlAlias);
  317. if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
  318. $indexValue = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]];
  319. $reflFieldValue->hydrateSet($indexValue, $element);
  320. $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue;
  321. } else {
  322. $reflFieldValue->hydrateAdd($element);
  323. $reflFieldValue->last();
  324. $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key();
  325. }
  326. // Update result pointer
  327. $this->resultPointers[$dqlAlias] = $element;
  328. }
  329. } else {
  330. // Update result pointer
  331. $this->resultPointers[$dqlAlias] = $reflFieldValue[$index];
  332. }
  333. } elseif (! $reflFieldValue) {
  334. $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
  335. } elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false) {
  336. $reflFieldValue->setInitialized(true);
  337. }
  338. } else {
  339. // PATH B: Single-valued association
  340. $reflFieldValue = $reflField->getValue($parentObject);
  341. if (! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH]) || ($reflFieldValue instanceof Proxy && ! $reflFieldValue->__isInitialized())) {
  342. // we only need to take action if this value is null,
  343. // we refresh the entity or its an uninitialized proxy.
  344. if (isset($nonemptyComponents[$dqlAlias])) {
  345. $element = $this->getEntity($data, $dqlAlias);
  346. $reflField->setValue($parentObject, $element);
  347. $this->_uow->setOriginalEntityProperty($oid, $relationField, $element);
  348. $targetClass = $this->_metadataCache[$relation['targetEntity']];
  349. if ($relation['isOwningSide']) {
  350. // TODO: Just check hints['fetched'] here?
  351. // If there is an inverse mapping on the target class its bidirectional
  352. if ($relation['inversedBy']) {
  353. $inverseAssoc = $targetClass->associationMappings[$relation['inversedBy']];
  354. if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) {
  355. $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject);
  356. $this->_uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc['fieldName'], $parentObject);
  357. }
  358. } elseif ($parentClass === $targetClass && $relation['mappedBy']) {
  359. // Special case: bi-directional self-referencing one-one on the same class
  360. $targetClass->reflFields[$relationField]->setValue($element, $parentObject);
  361. }
  362. } else {
  363. // For sure bidirectional, as there is no inverse side in unidirectional mappings
  364. $targetClass->reflFields[$relation['mappedBy']]->setValue($element, $parentObject);
  365. $this->_uow->setOriginalEntityProperty(spl_object_id($element), $relation['mappedBy'], $parentObject);
  366. }
  367. // Update result pointer
  368. $this->resultPointers[$dqlAlias] = $element;
  369. } else {
  370. $this->_uow->setOriginalEntityProperty($oid, $relationField, null);
  371. $reflField->setValue($parentObject, null);
  372. }
  373. // else leave $reflFieldValue null for single-valued associations
  374. } else {
  375. // Update result pointer
  376. $this->resultPointers[$dqlAlias] = $reflFieldValue;
  377. }
  378. }
  379. } else {
  380. // PATH C: Its a root result element
  381. $this->rootAliases[$dqlAlias] = true; // Mark as root alias
  382. $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0;
  383. // if this row has a NULL value for the root result id then make it a null result.
  384. if (! isset($nonemptyComponents[$dqlAlias])) {
  385. if ($this->resultSetMapping()->isMixed) {
  386. $result[] = [$entityKey => null];
  387. } else {
  388. $result[] = null;
  389. }
  390. $resultKey = $this->resultCounter;
  391. ++$this->resultCounter;
  392. continue;
  393. }
  394. // check for existing result from the iterations before
  395. if (! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) {
  396. $element = $this->getEntity($data, $dqlAlias);
  397. if ($this->resultSetMapping()->isMixed) {
  398. $element = [$entityKey => $element];
  399. }
  400. if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
  401. $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]];
  402. if (isset($this->_hints['collection'])) {
  403. $this->_hints['collection']->hydrateSet($resultKey, $element);
  404. }
  405. $result[$resultKey] = $element;
  406. } else {
  407. $resultKey = $this->resultCounter;
  408. ++$this->resultCounter;
  409. if (isset($this->_hints['collection'])) {
  410. $this->_hints['collection']->hydrateAdd($element);
  411. }
  412. $result[] = $element;
  413. }
  414. $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey;
  415. // Update result pointer
  416. $this->resultPointers[$dqlAlias] = $element;
  417. } else {
  418. // Update result pointer
  419. $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]];
  420. $this->resultPointers[$dqlAlias] = $result[$index];
  421. $resultKey = $index;
  422. }
  423. }
  424. if (isset($this->_hints[Query::HINT_INTERNAL_ITERATION]) && $this->_hints[Query::HINT_INTERNAL_ITERATION]) {
  425. $this->_uow->hydrationComplete();
  426. }
  427. }
  428. if (! isset($resultKey)) {
  429. $this->resultCounter++;
  430. }
  431. // Append scalar values to mixed result sets
  432. if (isset($rowData['scalars'])) {
  433. if (! isset($resultKey)) {
  434. $resultKey = isset($this->resultSetMapping()->indexByMap['scalars'])
  435. ? $row[$this->resultSetMapping()->indexByMap['scalars']]
  436. : $this->resultCounter - 1;
  437. }
  438. foreach ($rowData['scalars'] as $name => $value) {
  439. $result[$resultKey][$name] = $value;
  440. }
  441. }
  442. // Append new object to mixed result sets
  443. if (isset($rowData['newObjects'])) {
  444. if (! isset($resultKey)) {
  445. $resultKey = $this->resultCounter - 1;
  446. }
  447. $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
  448. foreach ($rowData['newObjects'] as $objIndex => $newObject) {
  449. $class = $newObject['class'];
  450. $args = $newObject['args'];
  451. $obj = $class->newInstanceArgs($args);
  452. if ($scalarCount === 0 && count($rowData['newObjects']) === 1) {
  453. $result[$resultKey] = $obj;
  454. continue;
  455. }
  456. $result[$resultKey][$objIndex] = $obj;
  457. }
  458. }
  459. }
  460. /**
  461. * When executed in a hydrate() loop we may have to clear internal state to
  462. * decrease memory consumption.
  463. *
  464. * @param mixed $eventArgs
  465. *
  466. * @return void
  467. */
  468. public function onClear($eventArgs)
  469. {
  470. parent::onClear($eventArgs);
  471. $aliases = array_keys($this->identifierMap);
  472. $this->identifierMap = array_fill_keys($aliases, []);
  473. }
  474. }