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

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