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

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