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

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