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

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