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

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