vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php line 923

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