vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php line 449

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