vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php line 238

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