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

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