vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php line 272

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM\Internal\Hydration;
  4. use BackedEnum;
  5. use Doctrine\DBAL\Driver\ResultStatement;
  6. use Doctrine\DBAL\ForwardCompatibility\Result as ForwardCompatibilityResult;
  7. use Doctrine\DBAL\Platforms\AbstractPlatform;
  8. use Doctrine\DBAL\Result;
  9. use Doctrine\DBAL\Types\Type;
  10. use Doctrine\Deprecations\Deprecation;
  11. use Doctrine\ORM\EntityManagerInterface;
  12. use Doctrine\ORM\Events;
  13. use Doctrine\ORM\Mapping\ClassMetadata;
  14. use Doctrine\ORM\Query\ResultSetMapping;
  15. use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker;
  16. use Doctrine\ORM\UnitOfWork;
  17. use Generator;
  18. use LogicException;
  19. use ReflectionClass;
  20. use TypeError;
  21. use function array_map;
  22. use function array_merge;
  23. use function count;
  24. use function end;
  25. use function get_debug_type;
  26. use function in_array;
  27. use function is_array;
  28. use function sprintf;
  29. /**
  30. * Base class for all hydrators. A hydrator is a class that provides some form
  31. * of transformation of an SQL result set into another structure.
  32. */
  33. abstract class AbstractHydrator
  34. {
  35. /**
  36. * The ResultSetMapping.
  37. *
  38. * @var ResultSetMapping|null
  39. */
  40. protected $_rsm;
  41. /**
  42. * The EntityManager instance.
  43. *
  44. * @var EntityManagerInterface
  45. */
  46. protected $_em;
  47. /**
  48. * The dbms Platform instance.
  49. *
  50. * @var AbstractPlatform
  51. */
  52. protected $_platform;
  53. /**
  54. * The UnitOfWork of the associated EntityManager.
  55. *
  56. * @var UnitOfWork
  57. */
  58. protected $_uow;
  59. /**
  60. * Local ClassMetadata cache to avoid going to the EntityManager all the time.
  61. *
  62. * @var array<string, ClassMetadata<object>>
  63. */
  64. protected $_metadataCache = [];
  65. /**
  66. * The cache used during row-by-row hydration.
  67. *
  68. * @var array<string, mixed[]|null>
  69. */
  70. protected $_cache = [];
  71. /**
  72. * The statement that provides the data to hydrate.
  73. *
  74. * @var Result|null
  75. */
  76. protected $_stmt;
  77. /**
  78. * The query hints.
  79. *
  80. * @var array<string, mixed>
  81. */
  82. protected $_hints = [];
  83. /**
  84. * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
  85. *
  86. * @param EntityManagerInterface $em The EntityManager to use.
  87. */
  88. public function __construct(EntityManagerInterface $em)
  89. {
  90. $this->_em = $em;
  91. $this->_platform = $em->getConnection()->getDatabasePlatform();
  92. $this->_uow = $em->getUnitOfWork();
  93. }
  94. /**
  95. * Initiates a row-by-row hydration.
  96. *
  97. * @deprecated
  98. *
  99. * @param Result|ResultStatement $stmt
  100. * @param ResultSetMapping $resultSetMapping
  101. * @psalm-param array<string, mixed> $hints
  102. *
  103. * @return IterableResult
  104. */
  105. public function iterate($stmt, $resultSetMapping, array $hints = [])
  106. {
  107. Deprecation::trigger(
  108. 'doctrine/orm',
  109. 'https://github.com/doctrine/orm/issues/8463',
  110. 'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use toIterable() instead.',
  111. __METHOD__
  112. );
  113. $this->_stmt = $stmt instanceof ResultStatement ? ForwardCompatibilityResult::ensure($stmt) : $stmt;
  114. $this->_rsm = $resultSetMapping;
  115. $this->_hints = $hints;
  116. $evm = $this->_em->getEventManager();
  117. $evm->addEventListener([Events::onClear], $this);
  118. $this->prepare();
  119. return new IterableResult($this);
  120. }
  121. /**
  122. * Initiates a row-by-row hydration.
  123. *
  124. * @param Result|ResultStatement $stmt
  125. * @psalm-param array<string, mixed> $hints
  126. *
  127. * @return Generator<array-key, mixed>
  128. *
  129. * @final
  130. */
  131. public function toIterable($stmt, ResultSetMapping $resultSetMapping, array $hints = []): iterable
  132. {
  133. if (! $stmt instanceof Result) {
  134. if (! $stmt instanceof ResultStatement) {
  135. throw new TypeError(sprintf(
  136. '%s: Expected parameter $stmt to be an instance of %s or %s, got %s',
  137. __METHOD__,
  138. Result::class,
  139. ResultStatement::class,
  140. get_debug_type($stmt)
  141. ));
  142. }
  143. Deprecation::trigger(
  144. 'doctrine/orm',
  145. 'https://github.com/doctrine/orm/pull/8796',
  146. '%s: Passing a result as $stmt that does not implement %s is deprecated and will cause a TypeError on 3.0',
  147. __METHOD__,
  148. Result::class
  149. );
  150. $stmt = ForwardCompatibilityResult::ensure($stmt);
  151. }
  152. $this->_stmt = $stmt;
  153. $this->_rsm = $resultSetMapping;
  154. $this->_hints = $hints;
  155. $evm = $this->_em->getEventManager();
  156. $evm->addEventListener([Events::onClear], $this);
  157. $this->prepare();
  158. try {
  159. while (true) {
  160. $row = $this->statement()->fetchAssociative();
  161. if ($row === false) {
  162. break;
  163. }
  164. $result = [];
  165. $this->hydrateRowData($row, $result);
  166. $this->cleanupAfterRowIteration();
  167. if (count($result) === 1) {
  168. if (count($resultSetMapping->indexByMap) === 0) {
  169. yield end($result);
  170. } else {
  171. yield from $result;
  172. }
  173. } else {
  174. yield $result;
  175. }
  176. }
  177. } finally {
  178. $this->cleanup();
  179. }
  180. }
  181. final protected function statement(): Result
  182. {
  183. if ($this->_stmt === null) {
  184. throw new LogicException('Uninitialized _stmt property');
  185. }
  186. return $this->_stmt;
  187. }
  188. final protected function resultSetMapping(): ResultSetMapping
  189. {
  190. if ($this->_rsm === null) {
  191. throw new LogicException('Uninitialized _rsm property');
  192. }
  193. return $this->_rsm;
  194. }
  195. /**
  196. * Hydrates all rows returned by the passed statement instance at once.
  197. *
  198. * @param Result|ResultStatement $stmt
  199. * @param ResultSetMapping $resultSetMapping
  200. * @psalm-param array<string, string> $hints
  201. *
  202. * @return mixed[]
  203. */
  204. public function hydrateAll($stmt, $resultSetMapping, array $hints = [])
  205. {
  206. if (! $stmt instanceof Result) {
  207. if (! $stmt instanceof ResultStatement) {
  208. throw new TypeError(sprintf(
  209. '%s: Expected parameter $stmt to be an instance of %s or %s, got %s',
  210. __METHOD__,
  211. Result::class,
  212. ResultStatement::class,
  213. get_debug_type($stmt)
  214. ));
  215. }
  216. Deprecation::trigger(
  217. 'doctrine/orm',
  218. 'https://github.com/doctrine/orm/pull/8796',
  219. '%s: Passing a result as $stmt that does not implement %s is deprecated and will cause a TypeError on 3.0',
  220. __METHOD__,
  221. Result::class
  222. );
  223. $stmt = ForwardCompatibilityResult::ensure($stmt);
  224. }
  225. $this->_stmt = $stmt;
  226. $this->_rsm = $resultSetMapping;
  227. $this->_hints = $hints;
  228. $this->_em->getEventManager()->addEventListener([Events::onClear], $this);
  229. $this->prepare();
  230. try {
  231. $result = $this->hydrateAllData();
  232. } finally {
  233. $this->cleanup();
  234. }
  235. return $result;
  236. }
  237. /**
  238. * Hydrates a single row returned by the current statement instance during
  239. * row-by-row hydration with {@link iterate()} or {@link toIterable()}.
  240. *
  241. * @deprecated
  242. *
  243. * @return mixed[]|false
  244. */
  245. public function hydrateRow()
  246. {
  247. Deprecation::triggerIfCalledFromOutside(
  248. 'doctrine/orm',
  249. 'https://github.com/doctrine/orm/pull/9072',
  250. '%s is deprecated.',
  251. __METHOD__
  252. );
  253. $row = $this->statement()->fetchAssociative();
  254. if ($row === false) {
  255. $this->cleanup();
  256. return false;
  257. }
  258. $result = [];
  259. $this->hydrateRowData($row, $result);
  260. return $result;
  261. }
  262. /**
  263. * When executed in a hydrate() loop we have to clear internal state to
  264. * decrease memory consumption.
  265. *
  266. * @param mixed $eventArgs
  267. *
  268. * @return void
  269. */
  270. public function onClear($eventArgs)
  271. {
  272. }
  273. /**
  274. * Executes one-time preparation tasks, once each time hydration is started
  275. * through {@link hydrateAll} or {@link iterate()}.
  276. *
  277. * @return void
  278. */
  279. protected function prepare()
  280. {
  281. }
  282. /**
  283. * Executes one-time cleanup tasks at the end of a hydration that was initiated
  284. * through {@link hydrateAll} or {@link iterate()}.
  285. *
  286. * @return void
  287. */
  288. protected function cleanup()
  289. {
  290. $this->statement()->free();
  291. $this->_stmt = null;
  292. $this->_rsm = null;
  293. $this->_cache = [];
  294. $this->_metadataCache = [];
  295. $this
  296. ->_em
  297. ->getEventManager()
  298. ->removeEventListener([Events::onClear], $this);
  299. }
  300. protected function cleanupAfterRowIteration(): void
  301. {
  302. }
  303. /**
  304. * Hydrates a single row from the current statement instance.
  305. *
  306. * Template method.
  307. *
  308. * @param mixed[] $row The row data.
  309. * @param mixed[] $result The result to fill.
  310. *
  311. * @return void
  312. *
  313. * @throws HydrationException
  314. */
  315. protected function hydrateRowData(array $row, array &$result)
  316. {
  317. throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
  318. }
  319. /**
  320. * Hydrates all rows from the current statement instance at once.
  321. *
  322. * @return mixed[]
  323. */
  324. abstract protected function hydrateAllData();
  325. /**
  326. * Processes a row of the result set.
  327. *
  328. * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
  329. * Puts the elements of a result row into a new array, grouped by the dql alias
  330. * they belong to. The column names in the result set are mapped to their
  331. * field names during this procedure as well as any necessary conversions on
  332. * the values applied. Scalar values are kept in a specific key 'scalars'.
  333. *
  334. * @param mixed[] $data SQL Result Row.
  335. * @psalm-param array<string, string> $id Dql-Alias => ID-Hash.
  336. * @psalm-param array<string, bool> $nonemptyComponents Does this DQL-Alias has at least one non NULL value?
  337. *
  338. * @return array<string, array<string, mixed>> An array with all the fields
  339. * (name => value) of the data
  340. * row, grouped by their
  341. * component alias.
  342. * @psalm-return array{
  343. * data: array<array-key, array>,
  344. * newObjects?: array<array-key, array{
  345. * class: mixed,
  346. * args?: array
  347. * }>,
  348. * scalars?: array
  349. * }
  350. */
  351. protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents)
  352. {
  353. $rowData = ['data' => []];
  354. foreach ($data as $key => $value) {
  355. $cacheKeyInfo = $this->hydrateColumnInfo($key);
  356. if ($cacheKeyInfo === null) {
  357. continue;
  358. }
  359. $fieldName = $cacheKeyInfo['fieldName'];
  360. switch (true) {
  361. case isset($cacheKeyInfo['isNewObjectParameter']):
  362. $argIndex = $cacheKeyInfo['argIndex'];
  363. $objIndex = $cacheKeyInfo['objIndex'];
  364. $type = $cacheKeyInfo['type'];
  365. $value = $type->convertToPHPValue($value, $this->_platform);
  366. if ($value !== null && isset($cacheKeyInfo['enumType'])) {
  367. $value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
  368. }
  369. $rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class'];
  370. $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
  371. break;
  372. case isset($cacheKeyInfo['isScalar']):
  373. $type = $cacheKeyInfo['type'];
  374. $value = $type->convertToPHPValue($value, $this->_platform);
  375. if ($value !== null && isset($cacheKeyInfo['enumType'])) {
  376. $value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
  377. }
  378. $rowData['scalars'][$fieldName] = $value;
  379. break;
  380. //case (isset($cacheKeyInfo['isMetaColumn'])):
  381. default:
  382. $dqlAlias = $cacheKeyInfo['dqlAlias'];
  383. $type = $cacheKeyInfo['type'];
  384. // If there are field name collisions in the child class, then we need
  385. // to only hydrate if we are looking at the correct discriminator value
  386. if (
  387. isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
  388. && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
  389. ) {
  390. break;
  391. }
  392. // in an inheritance hierarchy the same field could be defined several times.
  393. // We overwrite this value so long we don't have a non-null value, that value we keep.
  394. // Per definition it cannot be that a field is defined several times and has several values.
  395. if (isset($rowData['data'][$dqlAlias][$fieldName])) {
  396. break;
  397. }
  398. $rowData['data'][$dqlAlias][$fieldName] = $type
  399. ? $type->convertToPHPValue($value, $this->_platform)
  400. : $value;
  401. if ($rowData['data'][$dqlAlias][$fieldName] !== null && isset($cacheKeyInfo['enumType'])) {
  402. $rowData['data'][$dqlAlias][$fieldName] = $this->buildEnum($rowData['data'][$dqlAlias][$fieldName], $cacheKeyInfo['enumType']);
  403. }
  404. if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
  405. $id[$dqlAlias] .= '|' . $value;
  406. $nonemptyComponents[$dqlAlias] = true;
  407. }
  408. break;
  409. }
  410. }
  411. return $rowData;
  412. }
  413. /**
  414. * Processes a row of the result set.
  415. *
  416. * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
  417. * simply converts column names to field names and properly converts the
  418. * values according to their types. The resulting row has the same number
  419. * of elements as before.
  420. *
  421. * @param mixed[] $data
  422. * @psalm-param array<string, mixed> $data
  423. *
  424. * @return mixed[] The processed row.
  425. * @psalm-return array<string, mixed>
  426. */
  427. protected function gatherScalarRowData(&$data)
  428. {
  429. $rowData = [];
  430. foreach ($data as $key => $value) {
  431. $cacheKeyInfo = $this->hydrateColumnInfo($key);
  432. if ($cacheKeyInfo === null) {
  433. continue;
  434. }
  435. $fieldName = $cacheKeyInfo['fieldName'];
  436. // WARNING: BC break! We know this is the desired behavior to type convert values, but this
  437. // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
  438. if (! isset($cacheKeyInfo['isScalar'])) {
  439. $type = $cacheKeyInfo['type'];
  440. $value = $type ? $type->convertToPHPValue($value, $this->_platform) : $value;
  441. $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName;
  442. }
  443. $rowData[$fieldName] = $value;
  444. }
  445. return $rowData;
  446. }
  447. /**
  448. * Retrieve column information from ResultSetMapping.
  449. *
  450. * @param string $key Column name
  451. *
  452. * @return mixed[]|null
  453. * @psalm-return array<string, mixed>|null
  454. */
  455. protected function hydrateColumnInfo($key)
  456. {
  457. if (isset($this->_cache[$key])) {
  458. return $this->_cache[$key];
  459. }
  460. switch (true) {
  461. // NOTE: Most of the times it's a field mapping, so keep it first!!!
  462. case isset($this->_rsm->fieldMappings[$key]):
  463. $classMetadata = $this->getClassMetadata($this->_rsm->declaringClasses[$key]);
  464. $fieldName = $this->_rsm->fieldMappings[$key];
  465. $fieldMapping = $classMetadata->fieldMappings[$fieldName];
  466. $ownerMap = $this->_rsm->columnOwnerMap[$key];
  467. $columnInfo = [
  468. 'isIdentifier' => in_array($fieldName, $classMetadata->identifier, true),
  469. 'fieldName' => $fieldName,
  470. 'type' => Type::getType($fieldMapping['type']),
  471. 'dqlAlias' => $ownerMap,
  472. 'enumType' => $this->_rsm->enumMappings[$key] ?? null,
  473. ];
  474. // the current discriminator value must be saved in order to disambiguate fields hydration,
  475. // should there be field name collisions
  476. if ($classMetadata->parentClasses && isset($this->_rsm->discriminatorColumns[$ownerMap])) {
  477. return $this->_cache[$key] = array_merge(
  478. $columnInfo,
  479. [
  480. 'discriminatorColumn' => $this->_rsm->discriminatorColumns[$ownerMap],
  481. 'discriminatorValue' => $classMetadata->discriminatorValue,
  482. 'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
  483. ]
  484. );
  485. }
  486. return $this->_cache[$key] = $columnInfo;
  487. case isset($this->_rsm->newObjectMappings[$key]):
  488. // WARNING: A NEW object is also a scalar, so it must be declared before!
  489. $mapping = $this->_rsm->newObjectMappings[$key];
  490. return $this->_cache[$key] = [
  491. 'isScalar' => true,
  492. 'isNewObjectParameter' => true,
  493. 'fieldName' => $this->_rsm->scalarMappings[$key],
  494. 'type' => Type::getType($this->_rsm->typeMappings[$key]),
  495. 'argIndex' => $mapping['argIndex'],
  496. 'objIndex' => $mapping['objIndex'],
  497. 'class' => new ReflectionClass($mapping['className']),
  498. 'enumType' => $this->_rsm->enumMappings[$key] ?? null,
  499. ];
  500. case isset($this->_rsm->scalarMappings[$key], $this->_hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]):
  501. return $this->_cache[$key] = [
  502. 'fieldName' => $this->_rsm->scalarMappings[$key],
  503. 'type' => Type::getType($this->_rsm->typeMappings[$key]),
  504. 'dqlAlias' => '',
  505. 'enumType' => $this->_rsm->enumMappings[$key] ?? null,
  506. ];
  507. case isset($this->_rsm->scalarMappings[$key]):
  508. return $this->_cache[$key] = [
  509. 'isScalar' => true,
  510. 'fieldName' => $this->_rsm->scalarMappings[$key],
  511. 'type' => Type::getType($this->_rsm->typeMappings[$key]),
  512. 'enumType' => $this->_rsm->enumMappings[$key] ?? null,
  513. ];
  514. case isset($this->_rsm->metaMappings[$key]):
  515. // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
  516. $fieldName = $this->_rsm->metaMappings[$key];
  517. $dqlAlias = $this->_rsm->columnOwnerMap[$key];
  518. $type = isset($this->_rsm->typeMappings[$key])
  519. ? Type::getType($this->_rsm->typeMappings[$key])
  520. : null;
  521. // Cache metadata fetch
  522. $this->getClassMetadata($this->_rsm->aliasMap[$dqlAlias]);
  523. return $this->_cache[$key] = [
  524. 'isIdentifier' => isset($this->_rsm->isIdentifierColumn[$dqlAlias][$key]),
  525. 'isMetaColumn' => true,
  526. 'fieldName' => $fieldName,
  527. 'type' => $type,
  528. 'dqlAlias' => $dqlAlias,
  529. 'enumType' => $this->_rsm->enumMappings[$key] ?? null,
  530. ];
  531. }
  532. // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
  533. // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
  534. return null;
  535. }
  536. /**
  537. * @return string[]
  538. * @psalm-return non-empty-list<string>
  539. */
  540. private function getDiscriminatorValues(ClassMetadata $classMetadata): array
  541. {
  542. $values = array_map(
  543. function (string $subClass): string {
  544. return (string) $this->getClassMetadata($subClass)->discriminatorValue;
  545. },
  546. $classMetadata->subClasses
  547. );
  548. $values[] = (string) $classMetadata->discriminatorValue;
  549. return $values;
  550. }
  551. /**
  552. * Retrieve ClassMetadata associated to entity class name.
  553. *
  554. * @param string $className
  555. *
  556. * @return ClassMetadata
  557. */
  558. protected function getClassMetadata($className)
  559. {
  560. if (! isset($this->_metadataCache[$className])) {
  561. $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
  562. }
  563. return $this->_metadataCache[$className];
  564. }
  565. /**
  566. * Register entity as managed in UnitOfWork.
  567. *
  568. * @param object $entity
  569. * @param mixed[] $data
  570. *
  571. * @return void
  572. *
  573. * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
  574. */
  575. protected function registerManaged(ClassMetadata $class, $entity, array $data)
  576. {
  577. if ($class->isIdentifierComposite) {
  578. $id = [];
  579. foreach ($class->identifier as $fieldName) {
  580. $id[$fieldName] = isset($class->associationMappings[$fieldName])
  581. ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
  582. : $data[$fieldName];
  583. }
  584. } else {
  585. $fieldName = $class->identifier[0];
  586. $id = [
  587. $fieldName => isset($class->associationMappings[$fieldName])
  588. ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
  589. : $data[$fieldName],
  590. ];
  591. }
  592. $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data);
  593. }
  594. /**
  595. * @param mixed $value
  596. * @param class-string<BackedEnum> $enumType
  597. *
  598. * @return BackedEnum|array<BackedEnum>
  599. */
  600. final protected function buildEnum($value, string $enumType)
  601. {
  602. if (is_array($value)) {
  603. return array_map(static function ($value) use ($enumType): BackedEnum {
  604. return $enumType::from($value);
  605. }, $value);
  606. }
  607. return $enumType::from($value);
  608. }
  609. }