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

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