vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php line 912

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM\Persisters\Entity;
  4. use BackedEnum;
  5. use Doctrine\Common\Collections\Criteria;
  6. use Doctrine\Common\Collections\Expr\Comparison;
  7. use Doctrine\Common\Util\ClassUtils;
  8. use Doctrine\DBAL\Connection;
  9. use Doctrine\DBAL\LockMode;
  10. use Doctrine\DBAL\Platforms\AbstractPlatform;
  11. use Doctrine\DBAL\Result;
  12. use Doctrine\DBAL\Types\Type;
  13. use Doctrine\DBAL\Types\Types;
  14. use Doctrine\Deprecations\Deprecation;
  15. use Doctrine\ORM\EntityManagerInterface;
  16. use Doctrine\ORM\Mapping\ClassMetadata;
  17. use Doctrine\ORM\Mapping\MappingException;
  18. use Doctrine\ORM\Mapping\QuoteStrategy;
  19. use Doctrine\ORM\OptimisticLockException;
  20. use Doctrine\ORM\PersistentCollection;
  21. use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;
  22. use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
  23. use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
  24. use Doctrine\ORM\Persisters\SqlExpressionVisitor;
  25. use Doctrine\ORM\Persisters\SqlValueVisitor;
  26. use Doctrine\ORM\Query;
  27. use Doctrine\ORM\Query\QueryException;
  28. use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
  29. use Doctrine\ORM\UnitOfWork;
  30. use Doctrine\ORM\Utility\IdentifierFlattener;
  31. use Doctrine\ORM\Utility\PersisterHelper;
  32. use LengthException;
  33. use function array_combine;
  34. use function array_keys;
  35. use function array_map;
  36. use function array_merge;
  37. use function array_search;
  38. use function array_unique;
  39. use function array_values;
  40. use function assert;
  41. use function count;
  42. use function implode;
  43. use function is_array;
  44. use function is_object;
  45. use function reset;
  46. use function spl_object_id;
  47. use function sprintf;
  48. use function str_contains;
  49. use function strtoupper;
  50. use function trim;
  51. /**
  52. * A BasicEntityPersister maps an entity to a single table in a relational database.
  53. *
  54. * A persister is always responsible for a single entity type.
  55. *
  56. * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
  57. * state of entities onto a relational database when the UnitOfWork is committed,
  58. * as well as for basic querying of entities and their associations (not DQL).
  59. *
  60. * The persisting operations that are invoked during a commit of a UnitOfWork to
  61. * persist the persistent entity state are:
  62. *
  63. * - {@link addInsert} : To schedule an entity for insertion.
  64. * - {@link executeInserts} : To execute all scheduled insertions.
  65. * - {@link update} : To update the persistent state of an entity.
  66. * - {@link delete} : To delete the persistent state of an entity.
  67. *
  68. * As can be seen from the above list, insertions are batched and executed all at once
  69. * for increased efficiency.
  70. *
  71. * The querying operations invoked during a UnitOfWork, either through direct find
  72. * requests or lazy-loading, are the following:
  73. *
  74. * - {@link load} : Loads (the state of) a single, managed entity.
  75. * - {@link loadAll} : Loads multiple, managed entities.
  76. * - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
  77. * - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
  78. * - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
  79. *
  80. * The BasicEntityPersister implementation provides the default behavior for
  81. * persisting and querying entities that are mapped to a single database table.
  82. *
  83. * Subclasses can be created to provide custom persisting and querying strategies,
  84. * i.e. spanning multiple tables.
  85. */
  86. class BasicEntityPersister implements EntityPersister
  87. {
  88. /** @var array<string,string> */
  89. private static $comparisonMap = [
  90. Comparison::EQ => '= %s',
  91. Comparison::NEQ => '!= %s',
  92. Comparison::GT => '> %s',
  93. Comparison::GTE => '>= %s',
  94. Comparison::LT => '< %s',
  95. Comparison::LTE => '<= %s',
  96. Comparison::IN => 'IN (%s)',
  97. Comparison::NIN => 'NOT IN (%s)',
  98. Comparison::CONTAINS => 'LIKE %s',
  99. Comparison::STARTS_WITH => 'LIKE %s',
  100. Comparison::ENDS_WITH => 'LIKE %s',
  101. ];
  102. /**
  103. * Metadata object that describes the mapping of the mapped entity class.
  104. *
  105. * @var ClassMetadata
  106. */
  107. protected $class;
  108. /**
  109. * The underlying DBAL Connection of the used EntityManager.
  110. *
  111. * @var Connection $conn
  112. */
  113. protected $conn;
  114. /**
  115. * The database platform.
  116. *
  117. * @var AbstractPlatform
  118. */
  119. protected $platform;
  120. /**
  121. * The EntityManager instance.
  122. *
  123. * @var EntityManagerInterface
  124. */
  125. protected $em;
  126. /**
  127. * Queued inserts.
  128. *
  129. * @psalm-var array<int, object>
  130. */
  131. protected $queuedInserts = [];
  132. /**
  133. * The map of column names to DBAL mapping types of all prepared columns used
  134. * when INSERTing or UPDATEing an entity.
  135. *
  136. * @see prepareInsertData($entity)
  137. * @see prepareUpdateData($entity)
  138. *
  139. * @var mixed[]
  140. */
  141. protected $columnTypes = [];
  142. /**
  143. * The map of quoted column names.
  144. *
  145. * @see prepareInsertData($entity)
  146. * @see prepareUpdateData($entity)
  147. *
  148. * @var mixed[]
  149. */
  150. protected $quotedColumns = [];
  151. /**
  152. * The INSERT SQL statement used for entities handled by this persister.
  153. * This SQL is only generated once per request, if at all.
  154. *
  155. * @var string|null
  156. */
  157. private $insertSql;
  158. /**
  159. * The quote strategy.
  160. *
  161. * @var QuoteStrategy
  162. */
  163. protected $quoteStrategy;
  164. /**
  165. * The IdentifierFlattener used for manipulating identifiers
  166. *
  167. * @var IdentifierFlattener
  168. */
  169. private $identifierFlattener;
  170. /** @var CachedPersisterContext */
  171. protected $currentPersisterContext;
  172. /** @var CachedPersisterContext */
  173. private $limitsHandlingContext;
  174. /** @var CachedPersisterContext */
  175. private $noLimitsContext;
  176. /**
  177. * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
  178. * and persists instances of the class described by the given ClassMetadata descriptor.
  179. */
  180. public function __construct(EntityManagerInterface $em, ClassMetadata $class)
  181. {
  182. $this->em = $em;
  183. $this->class = $class;
  184. $this->conn = $em->getConnection();
  185. $this->platform = $this->conn->getDatabasePlatform();
  186. $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
  187. $this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
  188. $this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext(
  189. $class,
  190. new Query\ResultSetMapping(),
  191. false
  192. );
  193. $this->limitsHandlingContext = new CachedPersisterContext(
  194. $class,
  195. new Query\ResultSetMapping(),
  196. true
  197. );
  198. }
  199. /**
  200. * {@inheritdoc}
  201. */
  202. public function getClassMetadata()
  203. {
  204. return $this->class;
  205. }
  206. /**
  207. * {@inheritdoc}
  208. */
  209. public function getResultSetMapping()
  210. {
  211. return $this->currentPersisterContext->rsm;
  212. }
  213. /**
  214. * {@inheritdoc}
  215. */
  216. public function addInsert($entity)
  217. {
  218. $this->queuedInserts[spl_object_id($entity)] = $entity;
  219. }
  220. /**
  221. * {@inheritdoc}
  222. */
  223. public function getInserts()
  224. {
  225. return $this->queuedInserts;
  226. }
  227. /**
  228. * {@inheritdoc}
  229. */
  230. public function executeInserts()
  231. {
  232. if (! $this->queuedInserts) {
  233. return [];
  234. }
  235. $postInsertIds = [];
  236. $idGenerator = $this->class->idGenerator;
  237. $isPostInsertId = $idGenerator->isPostInsertGenerator();
  238. $stmt = $this->conn->prepare($this->getInsertSQL());
  239. $tableName = $this->class->getTableName();
  240. foreach ($this->queuedInserts as $entity) {
  241. $insertData = $this->prepareInsertData($entity);
  242. if (isset($insertData[$tableName])) {
  243. $paramIndex = 1;
  244. foreach ($insertData[$tableName] as $column => $value) {
  245. $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
  246. }
  247. }
  248. $stmt->executeStatement();
  249. if ($isPostInsertId) {
  250. $generatedId = $idGenerator->generateId($this->em, $entity);
  251. $id = [$this->class->identifier[0] => $generatedId];
  252. $postInsertIds[] = [
  253. 'generatedId' => $generatedId,
  254. 'entity' => $entity,
  255. ];
  256. } else {
  257. $id = $this->class->getIdentifierValues($entity);
  258. }
  259. if ($this->class->requiresFetchAfterChange) {
  260. $this->assignDefaultVersionAndUpsertableValues($entity, $id);
  261. }
  262. }
  263. $this->queuedInserts = [];
  264. return $postInsertIds;
  265. }
  266. /**
  267. * Retrieves the default version value which was created
  268. * by the preceding INSERT statement and assigns it back in to the
  269. * entities version field if the given entity is versioned.
  270. * Also retrieves values of columns marked as 'non insertable' and / or
  271. * 'not updatable' and assigns them back to the entities corresponding fields.
  272. *
  273. * @param object $entity
  274. * @param mixed[] $id
  275. *
  276. * @return void
  277. */
  278. protected function assignDefaultVersionAndUpsertableValues($entity, array $id)
  279. {
  280. $values = $this->fetchVersionAndNotUpsertableValues($this->class, $id);
  281. foreach ($values as $field => $value) {
  282. $value = Type::getType($this->class->fieldMappings[$field]['type'])->convertToPHPValue($value, $this->platform);
  283. $this->class->setFieldValue($entity, $field, $value);
  284. }
  285. }
  286. /**
  287. * Fetches the current version value of a versioned entity and / or the values of fields
  288. * marked as 'not insertable' and / or 'not updatable'.
  289. *
  290. * @param ClassMetadata $versionedClass
  291. * @param mixed[] $id
  292. *
  293. * @return mixed
  294. */
  295. protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id)
  296. {
  297. $columnNames = [];
  298. foreach ($this->class->fieldMappings as $key => $column) {
  299. if (isset($column['generated']) || ($this->class->isVersioned && $key === $versionedClass->versionField)) {
  300. $columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform);
  301. }
  302. }
  303. $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
  304. $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
  305. // FIXME: Order with composite keys might not be correct
  306. $sql = 'SELECT ' . implode(', ', $columnNames)
  307. . ' FROM ' . $tableName
  308. . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
  309. $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
  310. $values = $this->conn->fetchNumeric(
  311. $sql,
  312. array_values($flatId),
  313. $this->extractIdentifierTypes($id, $versionedClass)
  314. );
  315. if ($values === false) {
  316. throw new LengthException('Unexpected empty result for database query.');
  317. }
  318. $values = array_combine(array_keys($columnNames), $values);
  319. if (! $values) {
  320. throw new LengthException('Unexpected number of database columns.');
  321. }
  322. return $values;
  323. }
  324. /**
  325. * @param mixed[] $id
  326. *
  327. * @return int[]|null[]|string[]
  328. * @psalm-return list<int|string|null>
  329. */
  330. private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
  331. {
  332. $types = [];
  333. foreach ($id as $field => $value) {
  334. $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
  335. }
  336. return $types;
  337. }
  338. /**
  339. * {@inheritdoc}
  340. */
  341. public function update($entity)
  342. {
  343. $tableName = $this->class->getTableName();
  344. $updateData = $this->prepareUpdateData($entity);
  345. if (! isset($updateData[$tableName])) {
  346. return;
  347. }
  348. $data = $updateData[$tableName];
  349. if (! $data) {
  350. return;
  351. }
  352. $isVersioned = $this->class->isVersioned;
  353. $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
  354. $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
  355. if ($this->class->requiresFetchAfterChange) {
  356. $id = $this->class->getIdentifierValues($entity);
  357. $this->assignDefaultVersionAndUpsertableValues($entity, $id);
  358. }
  359. }
  360. /**
  361. * Performs an UPDATE statement for an entity on a specific table.
  362. * The UPDATE can optionally be versioned, which requires the entity to have a version field.
  363. *
  364. * @param object $entity The entity object being updated.
  365. * @param string $quotedTableName The quoted name of the table to apply the UPDATE on.
  366. * @param mixed[] $updateData The map of columns to update (column => value).
  367. * @param bool $versioned Whether the UPDATE should be versioned.
  368. *
  369. * @throws UnrecognizedField
  370. * @throws OptimisticLockException
  371. */
  372. final protected function updateTable(
  373. $entity,
  374. $quotedTableName,
  375. array $updateData,
  376. $versioned = false
  377. ): void {
  378. $set = [];
  379. $types = [];
  380. $params = [];
  381. foreach ($updateData as $columnName => $value) {
  382. $placeholder = '?';
  383. $column = $columnName;
  384. switch (true) {
  385. case isset($this->class->fieldNames[$columnName]):
  386. $fieldName = $this->class->fieldNames[$columnName];
  387. $column = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
  388. if (isset($this->class->fieldMappings[$fieldName]['requireSQLConversion'])) {
  389. $type = Type::getType($this->columnTypes[$columnName]);
  390. $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
  391. }
  392. break;
  393. case isset($this->quotedColumns[$columnName]):
  394. $column = $this->quotedColumns[$columnName];
  395. break;
  396. }
  397. $params[] = $value;
  398. $set[] = $column . ' = ' . $placeholder;
  399. $types[] = $this->columnTypes[$columnName];
  400. }
  401. $where = [];
  402. $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
  403. foreach ($this->class->identifier as $idField) {
  404. if (! isset($this->class->associationMappings[$idField])) {
  405. $params[] = $identifier[$idField];
  406. $types[] = $this->class->fieldMappings[$idField]['type'];
  407. $where[] = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform);
  408. continue;
  409. }
  410. $params[] = $identifier[$idField];
  411. $where[] = $this->quoteStrategy->getJoinColumnName(
  412. $this->class->associationMappings[$idField]['joinColumns'][0],
  413. $this->class,
  414. $this->platform
  415. );
  416. $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);
  417. $targetType = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em);
  418. if ($targetType === []) {
  419. throw UnrecognizedField::byFullyQualifiedName($this->class->name, $targetMapping->identifier[0]);
  420. }
  421. $types[] = reset($targetType);
  422. }
  423. if ($versioned) {
  424. $versionField = $this->class->versionField;
  425. assert($versionField !== null);
  426. $versionFieldType = $this->class->fieldMappings[$versionField]['type'];
  427. $versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform);
  428. $where[] = $versionColumn;
  429. $types[] = $this->class->fieldMappings[$versionField]['type'];
  430. $params[] = $this->class->reflFields[$versionField]->getValue($entity);
  431. switch ($versionFieldType) {
  432. case Types::SMALLINT:
  433. case Types::INTEGER:
  434. case Types::BIGINT:
  435. $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
  436. break;
  437. case Types::DATETIME_MUTABLE:
  438. $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
  439. break;
  440. }
  441. }
  442. $sql = 'UPDATE ' . $quotedTableName
  443. . ' SET ' . implode(', ', $set)
  444. . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
  445. $result = $this->conn->executeStatement($sql, $params, $types);
  446. if ($versioned && ! $result) {
  447. throw OptimisticLockException::lockFailed($entity);
  448. }
  449. }
  450. /**
  451. * @param array<mixed> $identifier
  452. * @param string[] $types
  453. *
  454. * @todo Add check for platform if it supports foreign keys/cascading.
  455. */
  456. protected function deleteJoinTableRecords(array $identifier, array $types): void
  457. {
  458. foreach ($this->class->associationMappings as $mapping) {
  459. if ($mapping['type'] !== ClassMetadata::MANY_TO_MANY) {
  460. continue;
  461. }
  462. // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
  463. // like self-referential relationship between different levels of an inheritance hierarchy? I hope not!
  464. $selfReferential = ($mapping['targetEntity'] === $mapping['sourceEntity']);
  465. $class = $this->class;
  466. $association = $mapping;
  467. $otherColumns = [];
  468. $otherKeys = [];
  469. $keys = [];
  470. if (! $mapping['isOwningSide']) {
  471. $class = $this->em->getClassMetadata($mapping['targetEntity']);
  472. $association = $class->associationMappings[$mapping['mappedBy']];
  473. }
  474. $joinColumns = $mapping['isOwningSide']
  475. ? $association['joinTable']['joinColumns']
  476. : $association['joinTable']['inverseJoinColumns'];
  477. if ($selfReferential) {
  478. $otherColumns = ! $mapping['isOwningSide']
  479. ? $association['joinTable']['joinColumns']
  480. : $association['joinTable']['inverseJoinColumns'];
  481. }
  482. foreach ($joinColumns as $joinColumn) {
  483. $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  484. }
  485. foreach ($otherColumns as $joinColumn) {
  486. $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  487. }
  488. if (isset($mapping['isOnDeleteCascade'])) {
  489. continue;
  490. }
  491. $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
  492. $this->conn->delete($joinTableName, array_combine($keys, $identifier), $types);
  493. if ($selfReferential) {
  494. $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier), $types);
  495. }
  496. }
  497. }
  498. /**
  499. * {@inheritdoc}
  500. */
  501. public function delete($entity)
  502. {
  503. $class = $this->class;
  504. $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
  505. $tableName = $this->quoteStrategy->getTableName($class, $this->platform);
  506. $idColumns = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform);
  507. $id = array_combine($idColumns, $identifier);
  508. $types = $this->getClassIdentifiersTypes($class);
  509. $this->deleteJoinTableRecords($identifier, $types);
  510. return (bool) $this->conn->delete($tableName, $id, $types);
  511. }
  512. /**
  513. * Prepares the changeset of an entity for database insertion (UPDATE).
  514. *
  515. * The changeset is obtained from the currently running UnitOfWork.
  516. *
  517. * During this preparation the array that is passed as the second parameter is filled with
  518. * <columnName> => <value> pairs, grouped by table name.
  519. *
  520. * Example:
  521. * <code>
  522. * array(
  523. * 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
  524. * 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
  525. * ...
  526. * )
  527. * </code>
  528. *
  529. * @param object $entity The entity for which to prepare the data.
  530. * @param bool $isInsert Whether the data to be prepared refers to an insert statement.
  531. *
  532. * @return mixed[][] The prepared data.
  533. * @psalm-return array<string, array<array-key, mixed|null>>
  534. */
  535. protected function prepareUpdateData($entity, bool $isInsert = false)
  536. {
  537. $versionField = null;
  538. $result = [];
  539. $uow = $this->em->getUnitOfWork();
  540. $versioned = $this->class->isVersioned;
  541. if ($versioned !== false) {
  542. $versionField = $this->class->versionField;
  543. }
  544. foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
  545. if (isset($versionField) && $versionField === $field) {
  546. continue;
  547. }
  548. if (isset($this->class->embeddedClasses[$field])) {
  549. continue;
  550. }
  551. $newVal = $change[1];
  552. if (! isset($this->class->associationMappings[$field])) {
  553. $fieldMapping = $this->class->fieldMappings[$field];
  554. $columnName = $fieldMapping['columnName'];
  555. if (! $isInsert && isset($fieldMapping['notUpdatable'])) {
  556. continue;
  557. }
  558. if ($isInsert && isset($fieldMapping['notInsertable'])) {
  559. continue;
  560. }
  561. $this->columnTypes[$columnName] = $fieldMapping['type'];
  562. $result[$this->getOwningTable($field)][$columnName] = $newVal;
  563. continue;
  564. }
  565. $assoc = $this->class->associationMappings[$field];
  566. // Only owning side of x-1 associations can have a FK column.
  567. if (! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {
  568. continue;
  569. }
  570. if ($newVal !== null) {
  571. $oid = spl_object_id($newVal);
  572. if (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {
  573. // The associated entity $newVal is not yet persisted, so we must
  574. // set $newVal = null, in order to insert a null value and schedule an
  575. // extra update on the UnitOfWork.
  576. $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
  577. $newVal = null;
  578. }
  579. }
  580. $newValId = null;
  581. if ($newVal !== null) {
  582. $newValId = $uow->getEntityIdentifier($newVal);
  583. }
  584. $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
  585. $owningTable = $this->getOwningTable($field);
  586. foreach ($assoc['joinColumns'] as $joinColumn) {
  587. $sourceColumn = $joinColumn['name'];
  588. $targetColumn = $joinColumn['referencedColumnName'];
  589. $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
  590. $this->quotedColumns[$sourceColumn] = $quotedColumn;
  591. $this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
  592. $result[$owningTable][$sourceColumn] = $newValId
  593. ? $newValId[$targetClass->getFieldForColumn($targetColumn)]
  594. : null;
  595. }
  596. }
  597. return $result;
  598. }
  599. /**
  600. * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
  601. * The changeset of the entity is obtained from the currently running UnitOfWork.
  602. *
  603. * The default insert data preparation is the same as for updates.
  604. *
  605. * @see prepareUpdateData
  606. *
  607. * @param object $entity The entity for which to prepare the data.
  608. *
  609. * @return mixed[][] The prepared data for the tables to update.
  610. * @psalm-return array<string, mixed[]>
  611. */
  612. protected function prepareInsertData($entity)
  613. {
  614. return $this->prepareUpdateData($entity, true);
  615. }
  616. /**
  617. * {@inheritdoc}
  618. */
  619. public function getOwningTable($fieldName)
  620. {
  621. return $this->class->getTableName();
  622. }
  623. /**
  624. * {@inheritdoc}
  625. */
  626. public function load(array $criteria, $entity = null, $assoc = null, array $hints = [], $lockMode = null, $limit = null, ?array $orderBy = null)
  627. {
  628. $this->switchPersisterContext(null, $limit);
  629. $sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);
  630. [$params, $types] = $this->expandParameters($criteria);
  631. $stmt = $this->conn->executeQuery($sql, $params, $types);
  632. if ($entity !== null) {
  633. $hints[Query::HINT_REFRESH] = true;
  634. $hints[Query::HINT_REFRESH_ENTITY] = $entity;
  635. }
  636. $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
  637. $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
  638. return $entities ? $entities[0] : null;
  639. }
  640. /**
  641. * {@inheritdoc}
  642. */
  643. public function loadById(array $identifier, $entity = null)
  644. {
  645. return $this->load($identifier, $entity);
  646. }
  647. /**
  648. * {@inheritdoc}
  649. */
  650. public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = [])
  651. {
  652. $foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity']);
  653. if ($foundEntity !== false) {
  654. return $foundEntity;
  655. }
  656. $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
  657. if ($assoc['isOwningSide']) {
  658. $isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']);
  659. // Mark inverse side as fetched in the hints, otherwise the UoW would
  660. // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
  661. $hints = [];
  662. if ($isInverseSingleValued) {
  663. $hints['fetched']['r'][$assoc['inversedBy']] = true;
  664. }
  665. $targetEntity = $this->load($identifier, null, $assoc, $hints);
  666. // Complete bidirectional association, if necessary
  667. if ($targetEntity !== null && $isInverseSingleValued) {
  668. $targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity);
  669. }
  670. return $targetEntity;
  671. }
  672. $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
  673. $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);
  674. $computedIdentifier = [];
  675. // TRICKY: since the association is specular source and target are flipped
  676. foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
  677. if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
  678. throw MappingException::joinColumnMustPointToMappedField(
  679. $sourceClass->name,
  680. $sourceKeyColumn
  681. );
  682. }
  683. $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
  684. $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
  685. }
  686. $targetEntity = $this->load($computedIdentifier, null, $assoc);
  687. if ($targetEntity !== null) {
  688. $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
  689. }
  690. return $targetEntity;
  691. }
  692. /**
  693. * {@inheritdoc}
  694. */
  695. public function refresh(array $id, $entity, $lockMode = null)
  696. {
  697. $sql = $this->getSelectSQL($id, null, $lockMode);
  698. [$params, $types] = $this->expandParameters($id);
  699. $stmt = $this->conn->executeQuery($sql, $params, $types);
  700. $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
  701. $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
  702. }
  703. /**
  704. * {@inheritDoc}
  705. */
  706. public function count($criteria = [])
  707. {
  708. $sql = $this->getCountSQL($criteria);
  709. [$params, $types] = $criteria instanceof Criteria
  710. ? $this->expandCriteriaParameters($criteria)
  711. : $this->expandParameters($criteria);
  712. return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
  713. }
  714. /**
  715. * {@inheritdoc}
  716. */
  717. public function loadCriteria(Criteria $criteria)
  718. {
  719. $orderBy = $criteria->getOrderings();
  720. $limit = $criteria->getMaxResults();
  721. $offset = $criteria->getFirstResult();
  722. $query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
  723. [$params, $types] = $this->expandCriteriaParameters($criteria);
  724. $stmt = $this->conn->executeQuery($query, $params, $types);
  725. $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
  726. return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);
  727. }
  728. /**
  729. * {@inheritdoc}
  730. */
  731. public function expandCriteriaParameters(Criteria $criteria)
  732. {
  733. $expression = $criteria->getWhereExpression();
  734. $sqlParams = [];
  735. $sqlTypes = [];
  736. if ($expression === null) {
  737. return [$sqlParams, $sqlTypes];
  738. }
  739. $valueVisitor = new SqlValueVisitor();
  740. $valueVisitor->dispatch($expression);
  741. [, $types] = $valueVisitor->getParamsAndTypes();
  742. foreach ($types as $type) {
  743. [$field, $value, $operator] = $type;
  744. if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
  745. continue;
  746. }
  747. $sqlParams = array_merge($sqlParams, $this->getValues($value));
  748. $sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
  749. }
  750. return [$sqlParams, $sqlTypes];
  751. }
  752. /**
  753. * {@inheritdoc}
  754. */
  755. public function loadAll(array $criteria = [], ?array $orderBy = null, $limit = null, $offset = null)
  756. {
  757. $this->switchPersisterContext($offset, $limit);
  758. $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
  759. [$params, $types] = $this->expandParameters($criteria);
  760. $stmt = $this->conn->executeQuery($sql, $params, $types);
  761. $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
  762. return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);
  763. }
  764. /**
  765. * {@inheritdoc}
  766. */
  767. public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
  768. {
  769. $this->switchPersisterContext($offset, $limit);
  770. $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
  771. return $this->loadArrayFromResult($assoc, $stmt);
  772. }
  773. /**
  774. * Loads an array of entities from a given DBAL statement.
  775. *
  776. * @param mixed[] $assoc
  777. *
  778. * @return mixed[]
  779. */
  780. private function loadArrayFromResult(array $assoc, Result $stmt): array
  781. {
  782. $rsm = $this->currentPersisterContext->rsm;
  783. $hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
  784. if (isset($assoc['indexBy'])) {
  785. $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
  786. $rsm->addIndexBy('r', $assoc['indexBy']);
  787. }
  788. return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
  789. }
  790. /**
  791. * Hydrates a collection from a given DBAL statement.
  792. *
  793. * @param mixed[] $assoc
  794. *
  795. * @return mixed[]
  796. */
  797. private function loadCollectionFromStatement(
  798. array $assoc,
  799. Result $stmt,
  800. PersistentCollection $coll
  801. ): array {
  802. $rsm = $this->currentPersisterContext->rsm;
  803. $hints = [
  804. UnitOfWork::HINT_DEFEREAGERLOAD => true,
  805. 'collection' => $coll,
  806. ];
  807. if (isset($assoc['indexBy'])) {
  808. $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
  809. $rsm->addIndexBy('r', $assoc['indexBy']);
  810. }
  811. return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
  812. }
  813. /**
  814. * {@inheritdoc}
  815. */
  816. public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
  817. {
  818. $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
  819. return $this->loadCollectionFromStatement($assoc, $stmt, $collection);
  820. }
  821. /**
  822. * @param object $sourceEntity
  823. * @psalm-param array<string, mixed> $assoc
  824. *
  825. * @return Result
  826. *
  827. * @throws MappingException
  828. */
  829. private function getManyToManyStatement(
  830. array $assoc,
  831. $sourceEntity,
  832. ?int $offset = null,
  833. ?int $limit = null
  834. ) {
  835. $this->switchPersisterContext($offset, $limit);
  836. $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
  837. $class = $sourceClass;
  838. $association = $assoc;
  839. $criteria = [];
  840. $parameters = [];
  841. if (! $assoc['isOwningSide']) {
  842. $class = $this->em->getClassMetadata($assoc['targetEntity']);
  843. $association = $class->associationMappings[$assoc['mappedBy']];
  844. }
  845. $joinColumns = $assoc['isOwningSide']
  846. ? $association['joinTable']['joinColumns']
  847. : $association['joinTable']['inverseJoinColumns'];
  848. $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
  849. foreach ($joinColumns as $joinColumn) {
  850. $sourceKeyColumn = $joinColumn['referencedColumnName'];
  851. $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  852. switch (true) {
  853. case $sourceClass->containsForeignIdentifier:
  854. $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
  855. $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
  856. if (isset($sourceClass->associationMappings[$field])) {
  857. $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
  858. $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
  859. }
  860. break;
  861. case isset($sourceClass->fieldNames[$sourceKeyColumn]):
  862. $field = $sourceClass->fieldNames[$sourceKeyColumn];
  863. $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
  864. break;
  865. default:
  866. throw MappingException::joinColumnMustPointToMappedField(
  867. $sourceClass->name,
  868. $sourceKeyColumn
  869. );
  870. }
  871. $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value;
  872. $parameters[] = [
  873. 'value' => $value,
  874. 'field' => $field,
  875. 'class' => $sourceClass,
  876. ];
  877. }
  878. $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
  879. [$params, $types] = $this->expandToManyParameters($parameters);
  880. return $this->conn->executeQuery($sql, $params, $types);
  881. }
  882. /**
  883. * {@inheritdoc}
  884. */
  885. public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit = null, $offset = null, ?array $orderBy = null)
  886. {
  887. $this->switchPersisterContext($offset, $limit);
  888. $lockSql = '';
  889. $joinSql = '';
  890. $orderBySql = '';
  891. if ($assoc !== null && $assoc['type'] === ClassMetadata::MANY_TO_MANY) {
  892. $joinSql = $this->getSelectManyToManyJoinSQL($assoc);
  893. }
  894. if (isset($assoc['orderBy'])) {
  895. $orderBy = $assoc['orderBy'];
  896. }
  897. if ($orderBy) {
  898. $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name));
  899. }
  900. $conditionSql = $criteria instanceof Criteria
  901. ? $this->getSelectConditionCriteriaSQL($criteria)
  902. : $this->getSelectConditionSQL($criteria, $assoc);
  903. switch ($lockMode) {
  904. case LockMode::PESSIMISTIC_READ:
  905. $lockSql = ' ' . $this->platform->getReadLockSQL();
  906. break;
  907. case LockMode::PESSIMISTIC_WRITE:
  908. $lockSql = ' ' . $this->platform->getWriteLockSQL();
  909. break;
  910. }
  911. $columnList = $this->getSelectColumnsSQL();
  912. $tableAlias = $this->getSQLTableAlias($this->class->name);
  913. $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
  914. $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
  915. if ($filterSql !== '') {
  916. $conditionSql = $conditionSql
  917. ? $conditionSql . ' AND ' . $filterSql
  918. : $filterSql;
  919. }
  920. $select = 'SELECT ' . $columnList;
  921. $from = ' FROM ' . $tableName . ' ' . $tableAlias;
  922. $join = $this->currentPersisterContext->selectJoinSql . $joinSql;
  923. $where = ($conditionSql ? ' WHERE ' . $conditionSql : '');
  924. $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE);
  925. $query = $select
  926. . $lock
  927. . $join
  928. . $where
  929. . $orderBySql;
  930. return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql;
  931. }
  932. /**
  933. * {@inheritDoc}
  934. */
  935. public function getCountSQL($criteria = [])
  936. {
  937. $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
  938. $tableAlias = $this->getSQLTableAlias($this->class->name);
  939. $conditionSql = $criteria instanceof Criteria
  940. ? $this->getSelectConditionCriteriaSQL($criteria)
  941. : $this->getSelectConditionSQL($criteria);
  942. $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
  943. if ($filterSql !== '') {
  944. $conditionSql = $conditionSql
  945. ? $conditionSql . ' AND ' . $filterSql
  946. : $filterSql;
  947. }
  948. return 'SELECT COUNT(*) '
  949. . 'FROM ' . $tableName . ' ' . $tableAlias
  950. . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
  951. }
  952. /**
  953. * Gets the ORDER BY SQL snippet for ordered collections.
  954. *
  955. * @psalm-param array<string, string> $orderBy
  956. *
  957. * @throws InvalidOrientation
  958. * @throws InvalidFindByCall
  959. * @throws UnrecognizedField
  960. */
  961. final protected function getOrderBySQL(array $orderBy, string $baseTableAlias): string
  962. {
  963. $orderByList = [];
  964. foreach ($orderBy as $fieldName => $orientation) {
  965. $orientation = strtoupper(trim($orientation));
  966. if ($orientation !== 'ASC' && $orientation !== 'DESC') {
  967. throw InvalidOrientation::fromClassNameAndField($this->class->name, $fieldName);
  968. }
  969. if (isset($this->class->fieldMappings[$fieldName])) {
  970. $tableAlias = isset($this->class->fieldMappings[$fieldName]['inherited'])
  971. ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]['inherited'])
  972. : $baseTableAlias;
  973. $columnName = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
  974. $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
  975. continue;
  976. }
  977. if (isset($this->class->associationMappings[$fieldName])) {
  978. if (! $this->class->associationMappings[$fieldName]['isOwningSide']) {
  979. throw InvalidFindByCall::fromInverseSideUsage($this->class->name, $fieldName);
  980. }
  981. $tableAlias = isset($this->class->associationMappings[$fieldName]['inherited'])
  982. ? $this->getSQLTableAlias($this->class->associationMappings[$fieldName]['inherited'])
  983. : $baseTableAlias;
  984. foreach ($this->class->associationMappings[$fieldName]['joinColumns'] as $joinColumn) {
  985. $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
  986. $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
  987. }
  988. continue;
  989. }
  990. throw UnrecognizedField::byFullyQualifiedName($this->class->name, $fieldName);
  991. }
  992. return ' ORDER BY ' . implode(', ', $orderByList);
  993. }
  994. /**
  995. * Gets the SQL fragment with the list of columns to select when querying for
  996. * an entity in this persister.
  997. *
  998. * Subclasses should override this method to alter or change the select column
  999. * list SQL fragment. Note that in the implementation of BasicEntityPersister
  1000. * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
  1001. * Subclasses may or may not do the same.
  1002. *
  1003. * @return string The SQL fragment.
  1004. */
  1005. protected function getSelectColumnsSQL()
  1006. {
  1007. if ($this->currentPersisterContext->selectColumnListSql !== null) {
  1008. return $this->currentPersisterContext->selectColumnListSql;
  1009. }
  1010. $columnList = [];
  1011. $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root
  1012. // Add regular columns to select list
  1013. foreach ($this->class->fieldNames as $field) {
  1014. $columnList[] = $this->getSelectColumnSQL($field, $this->class);
  1015. }
  1016. $this->currentPersisterContext->selectJoinSql = '';
  1017. $eagerAliasCounter = 0;
  1018. foreach ($this->class->associationMappings as $assocField => $assoc) {
  1019. $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class);
  1020. if ($assocColumnSQL) {
  1021. $columnList[] = $assocColumnSQL;
  1022. }
  1023. $isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];
  1024. $isAssocFromOneEager = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
  1025. if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
  1026. continue;
  1027. }
  1028. if ((($assoc['type'] & ClassMetadata::TO_MANY) > 0) && $this->currentPersisterContext->handlesLimits) {
  1029. continue;
  1030. }
  1031. $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
  1032. if ($eagerEntity->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
  1033. continue; // now this is why you shouldn't use inheritance
  1034. }
  1035. $assocAlias = 'e' . ($eagerAliasCounter++);
  1036. $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);
  1037. foreach ($eagerEntity->fieldNames as $field) {
  1038. $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias);
  1039. }
  1040. foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) {
  1041. $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL(
  1042. $eagerAssocField,
  1043. $eagerAssoc,
  1044. $eagerEntity,
  1045. $assocAlias
  1046. );
  1047. if ($eagerAssocColumnSQL) {
  1048. $columnList[] = $eagerAssocColumnSQL;
  1049. }
  1050. }
  1051. $association = $assoc;
  1052. $joinCondition = [];
  1053. if (isset($assoc['indexBy'])) {
  1054. $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc['indexBy']);
  1055. }
  1056. if (! $assoc['isOwningSide']) {
  1057. $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
  1058. $association = $eagerEntity->getAssociationMapping($assoc['mappedBy']);
  1059. }
  1060. $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias);
  1061. $joinTableName = $this->quoteStrategy->getTableName($eagerEntity, $this->platform);
  1062. if ($assoc['isOwningSide']) {
  1063. $tableAlias = $this->getSQLTableAlias($association['targetEntity'], $assocAlias);
  1064. $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association['joinColumns']);
  1065. foreach ($association['joinColumns'] as $joinColumn) {
  1066. $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
  1067. $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
  1068. $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'])
  1069. . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol;
  1070. }
  1071. // Add filter SQL
  1072. $filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias);
  1073. if ($filterSql) {
  1074. $joinCondition[] = $filterSql;
  1075. }
  1076. } else {
  1077. $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN';
  1078. foreach ($association['joinColumns'] as $joinColumn) {
  1079. $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
  1080. $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
  1081. $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '
  1082. . $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol;
  1083. }
  1084. }
  1085. $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
  1086. $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
  1087. }
  1088. $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
  1089. return $this->currentPersisterContext->selectColumnListSql;
  1090. }
  1091. /**
  1092. * Gets the SQL join fragment used when selecting entities from an association.
  1093. *
  1094. * @param string $field
  1095. * @param mixed[] $assoc
  1096. * @param string $alias
  1097. *
  1098. * @return string
  1099. */
  1100. protected function getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
  1101. {
  1102. if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
  1103. return '';
  1104. }
  1105. $columnList = [];
  1106. $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
  1107. $isIdentifier = isset($assoc['id']) && $assoc['id'] === true;
  1108. $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias === 'r' ? '' : $alias));
  1109. foreach ($assoc['joinColumns'] as $joinColumn) {
  1110. $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
  1111. $resultColumnName = $this->getSQLColumnAlias($joinColumn['name']);
  1112. $type = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
  1113. $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn['name'], $isIdentifier, $type);
  1114. $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName);
  1115. }
  1116. return implode(', ', $columnList);
  1117. }
  1118. /**
  1119. * Gets the SQL join fragment used when selecting entities from a
  1120. * many-to-many association.
  1121. *
  1122. * @psalm-param array<string, mixed> $manyToMany
  1123. *
  1124. * @return string
  1125. */
  1126. protected function getSelectManyToManyJoinSQL(array $manyToMany)
  1127. {
  1128. $conditions = [];
  1129. $association = $manyToMany;
  1130. $sourceTableAlias = $this->getSQLTableAlias($this->class->name);
  1131. if (! $manyToMany['isOwningSide']) {
  1132. $targetEntity = $this->em->getClassMetadata($manyToMany['targetEntity']);
  1133. $association = $targetEntity->associationMappings[$manyToMany['mappedBy']];
  1134. }
  1135. $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
  1136. $joinColumns = $manyToMany['isOwningSide']
  1137. ? $association['joinTable']['inverseJoinColumns']
  1138. : $association['joinTable']['joinColumns'];
  1139. foreach ($joinColumns as $joinColumn) {
  1140. $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
  1141. $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
  1142. $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn;
  1143. }
  1144. return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
  1145. }
  1146. /**
  1147. * {@inheritdoc}
  1148. */
  1149. public function getInsertSQL()
  1150. {
  1151. if ($this->insertSql !== null) {
  1152. return $this->insertSql;
  1153. }
  1154. $columns = $this->getInsertColumnList();
  1155. $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
  1156. if (empty($columns)) {
  1157. $identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
  1158. $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
  1159. return $this->insertSql;
  1160. }
  1161. $values = [];
  1162. $columns = array_unique($columns);
  1163. foreach ($columns as $column) {
  1164. $placeholder = '?';
  1165. if (
  1166. isset($this->class->fieldNames[$column])
  1167. && isset($this->columnTypes[$this->class->fieldNames[$column]])
  1168. && isset($this->class->fieldMappings[$this->class->fieldNames[$column]]['requireSQLConversion'])
  1169. ) {
  1170. $type = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]);
  1171. $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
  1172. }
  1173. $values[] = $placeholder;
  1174. }
  1175. $columns = implode(', ', $columns);
  1176. $values = implode(', ', $values);
  1177. $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
  1178. return $this->insertSql;
  1179. }
  1180. /**
  1181. * Gets the list of columns to put in the INSERT SQL statement.
  1182. *
  1183. * Subclasses should override this method to alter or change the list of
  1184. * columns placed in the INSERT statements used by the persister.
  1185. *
  1186. * @return string[] The list of columns.
  1187. * @psalm-return list<string>
  1188. */
  1189. protected function getInsertColumnList()
  1190. {
  1191. $columns = [];
  1192. foreach ($this->class->reflFields as $name => $field) {
  1193. if ($this->class->isVersioned && $this->class->versionField === $name) {
  1194. continue;
  1195. }
  1196. if (isset($this->class->embeddedClasses[$name])) {
  1197. continue;
  1198. }
  1199. if (isset($this->class->associationMappings[$name])) {
  1200. $assoc = $this->class->associationMappings[$name];
  1201. if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
  1202. foreach ($assoc['joinColumns'] as $joinColumn) {
  1203. $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
  1204. }
  1205. }
  1206. continue;
  1207. }
  1208. if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) {
  1209. if (isset($this->class->fieldMappings[$name]['notInsertable'])) {
  1210. continue;
  1211. }
  1212. $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
  1213. $this->columnTypes[$name] = $this->class->fieldMappings[$name]['type'];
  1214. }
  1215. }
  1216. return $columns;
  1217. }
  1218. /**
  1219. * Gets the SQL snippet of a qualified column name for the given field name.
  1220. *
  1221. * @param string $field The field name.
  1222. * @param ClassMetadata $class The class that declares this field. The table this class is
  1223. * mapped to must own the column for the given field.
  1224. * @param string $alias
  1225. *
  1226. * @return string
  1227. */
  1228. protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
  1229. {
  1230. $root = $alias === 'r' ? '' : $alias;
  1231. $tableAlias = $this->getSQLTableAlias($class->name, $root);
  1232. $fieldMapping = $class->fieldMappings[$field];
  1233. $sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
  1234. $columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']);
  1235. $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);
  1236. if (! empty($fieldMapping['enumType'])) {
  1237. $this->currentPersisterContext->rsm->addEnumResult($columnAlias, $fieldMapping['enumType']);
  1238. }
  1239. if (isset($fieldMapping['requireSQLConversion'])) {
  1240. $type = Type::getType($fieldMapping['type']);
  1241. $sql = $type->convertToPHPValueSQL($sql, $this->platform);
  1242. }
  1243. return $sql . ' AS ' . $columnAlias;
  1244. }
  1245. /**
  1246. * Gets the SQL table alias for the given class name.
  1247. *
  1248. * @param string $className
  1249. * @param string $assocName
  1250. *
  1251. * @return string The SQL table alias.
  1252. *
  1253. * @todo Reconsider. Binding table aliases to class names is not such a good idea.
  1254. */
  1255. protected function getSQLTableAlias($className, $assocName = '')
  1256. {
  1257. if ($assocName) {
  1258. $className .= '#' . $assocName;
  1259. }
  1260. if (isset($this->currentPersisterContext->sqlTableAliases[$className])) {
  1261. return $this->currentPersisterContext->sqlTableAliases[$className];
  1262. }
  1263. $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
  1264. $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias;
  1265. return $tableAlias;
  1266. }
  1267. /**
  1268. * {@inheritdoc}
  1269. */
  1270. public function lock(array $criteria, $lockMode)
  1271. {
  1272. $lockSql = '';
  1273. $conditionSql = $this->getSelectConditionSQL($criteria);
  1274. switch ($lockMode) {
  1275. case LockMode::PESSIMISTIC_READ:
  1276. $lockSql = $this->platform->getReadLockSQL();
  1277. break;
  1278. case LockMode::PESSIMISTIC_WRITE:
  1279. $lockSql = $this->platform->getWriteLockSQL();
  1280. break;
  1281. }
  1282. $lock = $this->getLockTablesSql($lockMode);
  1283. $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
  1284. $sql = 'SELECT 1 '
  1285. . $lock
  1286. . $where
  1287. . $lockSql;
  1288. [$params, $types] = $this->expandParameters($criteria);
  1289. $this->conn->executeQuery($sql, $params, $types);
  1290. }
  1291. /**
  1292. * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
  1293. *
  1294. * @param int|null $lockMode One of the Doctrine\DBAL\LockMode::* constants.
  1295. * @psalm-param LockMode::*|null $lockMode
  1296. *
  1297. * @return string
  1298. */
  1299. protected function getLockTablesSql($lockMode)
  1300. {
  1301. if ($lockMode === null) {
  1302. Deprecation::trigger(
  1303. 'doctrine/orm',
  1304. 'https://github.com/doctrine/orm/pull/9466',
  1305. 'Passing null as argument to %s is deprecated, pass LockMode::NONE instead.',
  1306. __METHOD__
  1307. );
  1308. $lockMode = LockMode::NONE;
  1309. }
  1310. return $this->platform->appendLockHint(
  1311. 'FROM '
  1312. . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' '
  1313. . $this->getSQLTableAlias($this->class->name),
  1314. $lockMode
  1315. );
  1316. }
  1317. /**
  1318. * Gets the Select Where Condition from a Criteria object.
  1319. *
  1320. * @return string
  1321. */
  1322. protected function getSelectConditionCriteriaSQL(Criteria $criteria)
  1323. {
  1324. $expression = $criteria->getWhereExpression();
  1325. if ($expression === null) {
  1326. return '';
  1327. }
  1328. $visitor = new SqlExpressionVisitor($this, $this->class);
  1329. return $visitor->dispatch($expression);
  1330. }
  1331. /**
  1332. * {@inheritdoc}
  1333. */
  1334. public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
  1335. {
  1336. $selectedColumns = [];
  1337. $columns = $this->getSelectConditionStatementColumnSQL($field, $assoc);
  1338. if (count($columns) > 1 && $comparison === Comparison::IN) {
  1339. /*
  1340. * @todo try to support multi-column IN expressions.
  1341. * Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B'))
  1342. */
  1343. throw CantUseInOperatorOnCompositeKeys::create();
  1344. }
  1345. foreach ($columns as $column) {
  1346. $placeholder = '?';
  1347. if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {
  1348. $type = Type::getType($this->class->fieldMappings[$field]['type']);
  1349. $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform);
  1350. }
  1351. if ($comparison !== null) {
  1352. // special case null value handling
  1353. if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) {
  1354. $selectedColumns[] = $column . ' IS NULL';
  1355. continue;
  1356. }
  1357. if ($comparison === Comparison::NEQ && $value === null) {
  1358. $selectedColumns[] = $column . ' IS NOT NULL';
  1359. continue;
  1360. }
  1361. $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
  1362. continue;
  1363. }
  1364. if (is_array($value)) {
  1365. $in = sprintf('%s IN (%s)', $column, $placeholder);
  1366. if (array_search(null, $value, true) !== false) {
  1367. $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
  1368. continue;
  1369. }
  1370. $selectedColumns[] = $in;
  1371. continue;
  1372. }
  1373. if ($value === null) {
  1374. $selectedColumns[] = sprintf('%s IS NULL', $column);
  1375. continue;
  1376. }
  1377. $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
  1378. }
  1379. return implode(' AND ', $selectedColumns);
  1380. }
  1381. /**
  1382. * Builds the left-hand-side of a where condition statement.
  1383. *
  1384. * @psalm-param array<string, mixed>|null $assoc
  1385. *
  1386. * @return string[]
  1387. * @psalm-return list<string>
  1388. *
  1389. * @throws InvalidFindByCall
  1390. * @throws UnrecognizedField
  1391. */
  1392. private function getSelectConditionStatementColumnSQL(
  1393. string $field,
  1394. ?array $assoc = null
  1395. ): array {
  1396. if (isset($this->class->fieldMappings[$field])) {
  1397. $className = $this->class->fieldMappings[$field]['inherited'] ?? $this->class->name;
  1398. return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)];
  1399. }
  1400. if (isset($this->class->associationMappings[$field])) {
  1401. $association = $this->class->associationMappings[$field];
  1402. // Many-To-Many requires join table check for joinColumn
  1403. $columns = [];
  1404. $class = $this->class;
  1405. if ($association['type'] === ClassMetadata::MANY_TO_MANY) {
  1406. if (! $association['isOwningSide']) {
  1407. $association = $assoc;
  1408. }
  1409. $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
  1410. $joinColumns = $assoc['isOwningSide']
  1411. ? $association['joinTable']['joinColumns']
  1412. : $association['joinTable']['inverseJoinColumns'];
  1413. foreach ($joinColumns as $joinColumn) {
  1414. $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  1415. }
  1416. } else {
  1417. if (! $association['isOwningSide']) {
  1418. throw InvalidFindByCall::fromInverseSideUsage(
  1419. $this->class->name,
  1420. $field
  1421. );
  1422. }
  1423. $className = $association['inherited'] ?? $this->class->name;
  1424. foreach ($association['joinColumns'] as $joinColumn) {
  1425. $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
  1426. }
  1427. }
  1428. return $columns;
  1429. }
  1430. if ($assoc !== null && ! str_contains($field, ' ') && ! str_contains($field, '(')) {
  1431. // very careless developers could potentially open up this normally hidden api for userland attacks,
  1432. // therefore checking for spaces and function calls which are not allowed.
  1433. // found a join column condition, not really a "field"
  1434. return [$field];
  1435. }
  1436. throw UnrecognizedField::byFullyQualifiedName($this->class->name, $field);
  1437. }
  1438. /**
  1439. * Gets the conditional SQL fragment used in the WHERE clause when selecting
  1440. * entities in this persister.
  1441. *
  1442. * Subclasses are supposed to override this method if they intend to change
  1443. * or alter the criteria by which entities are selected.
  1444. *
  1445. * @param mixed[]|null $assoc
  1446. * @psalm-param array<string, mixed> $criteria
  1447. * @psalm-param array<string, mixed>|null $assoc
  1448. *
  1449. * @return string
  1450. */
  1451. protected function getSelectConditionSQL(array $criteria, $assoc = null)
  1452. {
  1453. $conditions = [];
  1454. foreach ($criteria as $field => $value) {
  1455. $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc);
  1456. }
  1457. return implode(' AND ', $conditions);
  1458. }
  1459. /**
  1460. * {@inheritdoc}
  1461. */
  1462. public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
  1463. {
  1464. $this->switchPersisterContext($offset, $limit);
  1465. $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
  1466. return $this->loadArrayFromResult($assoc, $stmt);
  1467. }
  1468. /**
  1469. * {@inheritdoc}
  1470. */
  1471. public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
  1472. {
  1473. $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
  1474. return $this->loadCollectionFromStatement($assoc, $stmt, $collection);
  1475. }
  1476. /**
  1477. * Builds criteria and execute SQL statement to fetch the one to many entities from.
  1478. *
  1479. * @param object $sourceEntity
  1480. * @psalm-param array<string, mixed> $assoc
  1481. */
  1482. private function getOneToManyStatement(
  1483. array $assoc,
  1484. $sourceEntity,
  1485. ?int $offset = null,
  1486. ?int $limit = null
  1487. ): Result {
  1488. $this->switchPersisterContext($offset, $limit);
  1489. $criteria = [];
  1490. $parameters = [];
  1491. $owningAssoc = $this->class->associationMappings[$assoc['mappedBy']];
  1492. $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
  1493. $tableAlias = $this->getSQLTableAlias($owningAssoc['inherited'] ?? $this->class->name);
  1494. foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
  1495. if ($sourceClass->containsForeignIdentifier) {
  1496. $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
  1497. $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
  1498. if (isset($sourceClass->associationMappings[$field])) {
  1499. $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
  1500. $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
  1501. }
  1502. $criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
  1503. $parameters[] = [
  1504. 'value' => $value,
  1505. 'field' => $field,
  1506. 'class' => $sourceClass,
  1507. ];
  1508. continue;
  1509. }
  1510. $field = $sourceClass->fieldNames[$sourceKeyColumn];
  1511. $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
  1512. $criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
  1513. $parameters[] = [
  1514. 'value' => $value,
  1515. 'field' => $field,
  1516. 'class' => $sourceClass,
  1517. ];
  1518. }
  1519. $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
  1520. [$params, $types] = $this->expandToManyParameters($parameters);
  1521. return $this->conn->executeQuery($sql, $params, $types);
  1522. }
  1523. /**
  1524. * {@inheritdoc}
  1525. */
  1526. public function expandParameters($criteria)
  1527. {
  1528. $params = [];
  1529. $types = [];
  1530. foreach ($criteria as $field => $value) {
  1531. if ($value === null) {
  1532. continue; // skip null values.
  1533. }
  1534. $types = array_merge($types, $this->getTypes($field, $value, $this->class));
  1535. $params = array_merge($params, $this->getValues($value));
  1536. }
  1537. return [$params, $types];
  1538. }
  1539. /**
  1540. * Expands the parameters from the given criteria and use the correct binding types if found,
  1541. * specialized for OneToMany or ManyToMany associations.
  1542. *
  1543. * @param mixed[][] $criteria an array of arrays containing following:
  1544. * - field to which each criterion will be bound
  1545. * - value to be bound
  1546. * - class to which the field belongs to
  1547. *
  1548. * @return mixed[][]
  1549. * @psalm-return array{0: array, 1: list<int|string|null>}
  1550. */
  1551. private function expandToManyParameters(array $criteria): array
  1552. {
  1553. $params = [];
  1554. $types = [];
  1555. foreach ($criteria as $criterion) {
  1556. if ($criterion['value'] === null) {
  1557. continue; // skip null values.
  1558. }
  1559. $types = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
  1560. $params = array_merge($params, $this->getValues($criterion['value']));
  1561. }
  1562. return [$params, $types];
  1563. }
  1564. /**
  1565. * Infers field types to be used by parameter type casting.
  1566. *
  1567. * @param mixed $value
  1568. *
  1569. * @return int[]|null[]|string[]
  1570. * @psalm-return list<int|string|null>
  1571. *
  1572. * @throws QueryException
  1573. */
  1574. private function getTypes(string $field, $value, ClassMetadata $class): array
  1575. {
  1576. $types = [];
  1577. switch (true) {
  1578. case isset($class->fieldMappings[$field]):
  1579. $types = array_merge($types, [$class->fieldMappings[$field]['type']]);
  1580. break;
  1581. case isset($class->associationMappings[$field]):
  1582. $assoc = $class->associationMappings[$field];
  1583. $class = $this->em->getClassMetadata($assoc['targetEntity']);
  1584. if (! $assoc['isOwningSide']) {
  1585. $assoc = $class->associationMappings[$assoc['mappedBy']];
  1586. $class = $this->em->getClassMetadata($assoc['targetEntity']);
  1587. }
  1588. $columns = $assoc['type'] === ClassMetadata::MANY_TO_MANY
  1589. ? $assoc['relationToTargetKeyColumns']
  1590. : $assoc['sourceToTargetKeyColumns'];
  1591. foreach ($columns as $column) {
  1592. $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);
  1593. }
  1594. break;
  1595. default:
  1596. $types[] = null;
  1597. break;
  1598. }
  1599. if (is_array($value)) {
  1600. return array_map(static function ($type) {
  1601. $type = Type::getType($type);
  1602. return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
  1603. }, $types);
  1604. }
  1605. return $types;
  1606. }
  1607. /**
  1608. * Retrieves the parameters that identifies a value.
  1609. *
  1610. * @param mixed $value
  1611. *
  1612. * @return mixed[]
  1613. */
  1614. private function getValues($value): array
  1615. {
  1616. if (is_array($value)) {
  1617. $newValue = [];
  1618. foreach ($value as $itemValue) {
  1619. $newValue = array_merge($newValue, $this->getValues($itemValue));
  1620. }
  1621. return [$newValue];
  1622. }
  1623. return $this->getIndividualValue($value);
  1624. }
  1625. /**
  1626. * Retrieves an individual parameter value.
  1627. *
  1628. * @param mixed $value
  1629. *
  1630. * @psalm-return list<mixed>
  1631. */
  1632. private function getIndividualValue($value): array
  1633. {
  1634. if (! is_object($value)) {
  1635. return [$value];
  1636. }
  1637. if ($value instanceof BackedEnum) {
  1638. return [$value->value];
  1639. }
  1640. $valueClass = ClassUtils::getClass($value);
  1641. if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
  1642. return [$value];
  1643. }
  1644. $class = $this->em->getClassMetadata($valueClass);
  1645. if ($class->isIdentifierComposite) {
  1646. $newValue = [];
  1647. foreach ($class->getIdentifierValues($value) as $innerValue) {
  1648. $newValue = array_merge($newValue, $this->getValues($innerValue));
  1649. }
  1650. return $newValue;
  1651. }
  1652. return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)];
  1653. }
  1654. /**
  1655. * {@inheritdoc}
  1656. */
  1657. public function exists($entity, ?Criteria $extraConditions = null)
  1658. {
  1659. $criteria = $this->class->getIdentifierValues($entity);
  1660. if (! $criteria) {
  1661. return false;
  1662. }
  1663. $alias = $this->getSQLTableAlias($this->class->name);
  1664. $sql = 'SELECT 1 '
  1665. . $this->getLockTablesSql(LockMode::NONE)
  1666. . ' WHERE ' . $this->getSelectConditionSQL($criteria);
  1667. [$params, $types] = $this->expandParameters($criteria);
  1668. if ($extraConditions !== null) {
  1669. $sql .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
  1670. [$criteriaParams, $criteriaTypes] = $this->expandCriteriaParameters($extraConditions);
  1671. $params = array_merge($params, $criteriaParams);
  1672. $types = array_merge($types, $criteriaTypes);
  1673. }
  1674. $filterSql = $this->generateFilterConditionSQL($this->class, $alias);
  1675. if ($filterSql) {
  1676. $sql .= ' AND ' . $filterSql;
  1677. }
  1678. return (bool) $this->conn->fetchOne($sql, $params, $types);
  1679. }
  1680. /**
  1681. * Generates the appropriate join SQL for the given join column.
  1682. *
  1683. * @param array[] $joinColumns The join columns definition of an association.
  1684. * @psalm-param array<array<string, mixed>> $joinColumns
  1685. *
  1686. * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
  1687. */
  1688. protected function getJoinSQLForJoinColumns($joinColumns)
  1689. {
  1690. // if one of the join columns is nullable, return left join
  1691. foreach ($joinColumns as $joinColumn) {
  1692. if (! isset($joinColumn['nullable']) || $joinColumn['nullable']) {
  1693. return 'LEFT JOIN';
  1694. }
  1695. }
  1696. return 'INNER JOIN';
  1697. }
  1698. /**
  1699. * @param string $columnName
  1700. *
  1701. * @return string
  1702. */
  1703. public function getSQLColumnAlias($columnName)
  1704. {
  1705. return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform);
  1706. }
  1707. /**
  1708. * Generates the filter SQL for a given entity and table alias.
  1709. *
  1710. * @param ClassMetadata $targetEntity Metadata of the target entity.
  1711. * @param string $targetTableAlias The table alias of the joined/selected table.
  1712. *
  1713. * @return string The SQL query part to add to a query.
  1714. */
  1715. protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
  1716. {
  1717. $filterClauses = [];
  1718. foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
  1719. $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
  1720. if ($filterExpr !== '') {
  1721. $filterClauses[] = '(' . $filterExpr . ')';
  1722. }
  1723. }
  1724. $sql = implode(' AND ', $filterClauses);
  1725. return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL"
  1726. }
  1727. /**
  1728. * Switches persister context according to current query offset/limits
  1729. *
  1730. * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
  1731. *
  1732. * @param int|null $offset
  1733. * @param int|null $limit
  1734. *
  1735. * @return void
  1736. */
  1737. protected function switchPersisterContext($offset, $limit)
  1738. {
  1739. if ($offset === null && $limit === null) {
  1740. $this->currentPersisterContext = $this->noLimitsContext;
  1741. return;
  1742. }
  1743. $this->currentPersisterContext = $this->limitsHandlingContext;
  1744. }
  1745. /**
  1746. * @return string[]
  1747. * @psalm-return list<string>
  1748. */
  1749. protected function getClassIdentifiersTypes(ClassMetadata $class): array
  1750. {
  1751. $entityManager = $this->em;
  1752. return array_map(
  1753. static function ($fieldName) use ($class, $entityManager): string {
  1754. $types = PersisterHelper::getTypeOfField($fieldName, $class, $entityManager);
  1755. assert(isset($types[0]));
  1756. return $types[0];
  1757. },
  1758. $class->identifier
  1759. );
  1760. }
  1761. }