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

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