vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php line 920
<?phpdeclare(strict_types=1);namespace Doctrine\ORM\Persisters\Entity;use BackedEnum;use Doctrine\Common\Collections\Criteria;use Doctrine\Common\Collections\Expr\Comparison;use Doctrine\Common\Util\ClassUtils;use Doctrine\DBAL\Connection;use Doctrine\DBAL\LockMode;use Doctrine\DBAL\Platforms\AbstractPlatform;use Doctrine\DBAL\Result;use Doctrine\DBAL\Types\Type;use Doctrine\DBAL\Types\Types;use Doctrine\Deprecations\Deprecation;use Doctrine\ORM\EntityManagerInterface;use Doctrine\ORM\Mapping\ClassMetadata;use Doctrine\ORM\Mapping\MappingException;use Doctrine\ORM\Mapping\QuoteStrategy;use Doctrine\ORM\OptimisticLockException;use Doctrine\ORM\PersistentCollection;use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;use Doctrine\ORM\Persisters\Exception\InvalidOrientation;use Doctrine\ORM\Persisters\Exception\UnrecognizedField;use Doctrine\ORM\Persisters\SqlExpressionVisitor;use Doctrine\ORM\Persisters\SqlValueVisitor;use Doctrine\ORM\Query;use Doctrine\ORM\Query\QueryException;use Doctrine\ORM\Repository\Exception\InvalidFindByCall;use Doctrine\ORM\UnitOfWork;use Doctrine\ORM\Utility\IdentifierFlattener;use Doctrine\ORM\Utility\PersisterHelper;use LengthException;use function array_combine;use function array_keys;use function array_map;use function array_merge;use function array_search;use function array_unique;use function array_values;use function assert;use function count;use function implode;use function is_array;use function is_object;use function reset;use function spl_object_id;use function sprintf;use function str_contains;use function strtoupper;use function trim;/*** A BasicEntityPersister maps an entity to a single table in a relational database.** A persister is always responsible for a single entity type.** EntityPersisters are used during a UnitOfWork to apply any changes to the persistent* state of entities onto a relational database when the UnitOfWork is committed,* as well as for basic querying of entities and their associations (not DQL).** The persisting operations that are invoked during a commit of a UnitOfWork to* persist the persistent entity state are:** - {@link addInsert} : To schedule an entity for insertion.* - {@link executeInserts} : To execute all scheduled insertions.* - {@link update} : To update the persistent state of an entity.* - {@link delete} : To delete the persistent state of an entity.** As can be seen from the above list, insertions are batched and executed all at once* for increased efficiency.** The querying operations invoked during a UnitOfWork, either through direct find* requests or lazy-loading, are the following:** - {@link load} : Loads (the state of) a single, managed entity.* - {@link loadAll} : Loads multiple, managed entities.* - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).* - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).* - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).** The BasicEntityPersister implementation provides the default behavior for* persisting and querying entities that are mapped to a single database table.** Subclasses can be created to provide custom persisting and querying strategies,* i.e. spanning multiple tables.*/class BasicEntityPersister implements EntityPersister{/** @var array<string,string> */private static $comparisonMap = [Comparison::EQ => '= %s',Comparison::NEQ => '!= %s',Comparison::GT => '> %s',Comparison::GTE => '>= %s',Comparison::LT => '< %s',Comparison::LTE => '<= %s',Comparison::IN => 'IN (%s)',Comparison::NIN => 'NOT IN (%s)',Comparison::CONTAINS => 'LIKE %s',Comparison::STARTS_WITH => 'LIKE %s',Comparison::ENDS_WITH => 'LIKE %s',];/*** Metadata object that describes the mapping of the mapped entity class.** @var ClassMetadata*/protected $class;/*** The underlying DBAL Connection of the used EntityManager.** @var Connection $conn*/protected $conn;/*** The database platform.** @var AbstractPlatform*/protected $platform;/*** The EntityManager instance.** @var EntityManagerInterface*/protected $em;/*** Queued inserts.** @psalm-var array<int, object>*/protected $queuedInserts = [];/*** The map of column names to DBAL mapping types of all prepared columns used* when INSERTing or UPDATEing an entity.** @see prepareInsertData($entity)* @see prepareUpdateData($entity)** @var mixed[]*/protected $columnTypes = [];/*** The map of quoted column names.** @see prepareInsertData($entity)* @see prepareUpdateData($entity)** @var mixed[]*/protected $quotedColumns = [];/*** The INSERT SQL statement used for entities handled by this persister.* This SQL is only generated once per request, if at all.** @var string|null*/private $insertSql;/*** The quote strategy.** @var QuoteStrategy*/protected $quoteStrategy;/*** The IdentifierFlattener used for manipulating identifiers** @var IdentifierFlattener*/private $identifierFlattener;/** @var CachedPersisterContext */protected $currentPersisterContext;/** @var CachedPersisterContext */private $limitsHandlingContext;/** @var CachedPersisterContext */private $noLimitsContext;/*** Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager* and persists instances of the class described by the given ClassMetadata descriptor.*/public function __construct(EntityManagerInterface $em, ClassMetadata $class){$this->em = $em;$this->class = $class;$this->conn = $em->getConnection();$this->platform = $this->conn->getDatabasePlatform();$this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();$this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());$this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext($class,new Query\ResultSetMapping(),false);$this->limitsHandlingContext = new CachedPersisterContext($class,new Query\ResultSetMapping(),true);}/*** {@inheritdoc}*/public function getClassMetadata(){return $this->class;}/*** {@inheritdoc}*/public function getResultSetMapping(){return $this->currentPersisterContext->rsm;}/*** {@inheritdoc}*/public function addInsert($entity){$this->queuedInserts[spl_object_id($entity)] = $entity;}/*** {@inheritdoc}*/public function getInserts(){return $this->queuedInserts;}/*** {@inheritdoc}*/public function executeInserts(){if (! $this->queuedInserts) {return [];}$postInsertIds = [];$idGenerator = $this->class->idGenerator;$isPostInsertId = $idGenerator->isPostInsertGenerator();$stmt = $this->conn->prepare($this->getInsertSQL());$tableName = $this->class->getTableName();foreach ($this->queuedInserts as $entity) {$insertData = $this->prepareInsertData($entity);if (isset($insertData[$tableName])) {$paramIndex = 1;foreach ($insertData[$tableName] as $column => $value) {$stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);}}$stmt->executeStatement();if ($isPostInsertId) {$generatedId = $idGenerator->generateId($this->em, $entity);$id = [$this->class->identifier[0] => $generatedId];$postInsertIds[] = ['generatedId' => $generatedId,'entity' => $entity,];} else {$id = $this->class->getIdentifierValues($entity);}if ($this->class->requiresFetchAfterChange) {$this->assignDefaultVersionAndUpsertableValues($entity, $id);}}$this->queuedInserts = [];return $postInsertIds;}/*** Retrieves the default version value which was created* by the preceding INSERT statement and assigns it back in to the* entities version field if the given entity is versioned.* Also retrieves values of columns marked as 'non insertable' and / or* 'not updatable' and assigns them back to the entities corresponding fields.** @param object $entity* @param mixed[] $id** @return void*/protected function assignDefaultVersionAndUpsertableValues($entity, array $id){$values = $this->fetchVersionAndNotUpsertableValues($this->class, $id);foreach ($values as $field => $value) {$value = Type::getType($this->class->fieldMappings[$field]['type'])->convertToPHPValue($value, $this->platform);$this->class->setFieldValue($entity, $field, $value);}}/*** Fetches the current version value of a versioned entity and / or the values of fields* marked as 'not insertable' and / or 'not updatable'.** @param ClassMetadata $versionedClass* @param mixed[] $id** @return mixed*/protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id){$columnNames = [];foreach ($this->class->fieldMappings as $key => $column) {if (isset($column['generated']) || ($this->class->isVersioned && $key === $versionedClass->versionField)) {$columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform);}}$tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);$identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);// FIXME: Order with composite keys might not be correct$sql = 'SELECT ' . implode(', ', $columnNames). ' FROM ' . $tableName. ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';$flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);$values = $this->conn->fetchNumeric($sql,array_values($flatId),$this->extractIdentifierTypes($id, $versionedClass));if ($values === false) {throw new LengthException('Unexpected empty result for database query.');}$values = array_combine(array_keys($columnNames), $values);if (! $values) {throw new LengthException('Unexpected number of database columns.');}return $values;}/*** @param mixed[] $id** @return int[]|null[]|string[]* @psalm-return list<int|string|null>*/private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array{$types = [];foreach ($id as $field => $value) {$types = array_merge($types, $this->getTypes($field, $value, $versionedClass));}return $types;}/*** {@inheritdoc}*/public function update($entity){$tableName = $this->class->getTableName();$updateData = $this->prepareUpdateData($entity);if (! isset($updateData[$tableName])) {return;}$data = $updateData[$tableName];if (! $data) {return;}$isVersioned = $this->class->isVersioned;$quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform);$this->updateTable($entity, $quotedTableName, $data, $isVersioned);if ($this->class->requiresFetchAfterChange) {$id = $this->class->getIdentifierValues($entity);$this->assignDefaultVersionAndUpsertableValues($entity, $id);}}/*** Performs an UPDATE statement for an entity on a specific table.* The UPDATE can optionally be versioned, which requires the entity to have a version field.** @param object $entity The entity object being updated.* @param string $quotedTableName The quoted name of the table to apply the UPDATE on.* @param mixed[] $updateData The map of columns to update (column => value).* @param bool $versioned Whether the UPDATE should be versioned.** @throws UnrecognizedField* @throws OptimisticLockException*/final protected function updateTable($entity,$quotedTableName,array $updateData,$versioned = false): void {$set = [];$types = [];$params = [];foreach ($updateData as $columnName => $value) {$placeholder = '?';$column = $columnName;switch (true) {case isset($this->class->fieldNames[$columnName]):$fieldName = $this->class->fieldNames[$columnName];$column = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);if (isset($this->class->fieldMappings[$fieldName]['requireSQLConversion'])) {$type = Type::getType($this->columnTypes[$columnName]);$placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);}break;case isset($this->quotedColumns[$columnName]):$column = $this->quotedColumns[$columnName];break;}$params[] = $value;$set[] = $column . ' = ' . $placeholder;$types[] = $this->columnTypes[$columnName];}$where = [];$identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);foreach ($this->class->identifier as $idField) {if (! isset($this->class->associationMappings[$idField])) {$params[] = $identifier[$idField];$types[] = $this->class->fieldMappings[$idField]['type'];$where[] = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform);continue;}$params[] = $identifier[$idField];$where[] = $this->quoteStrategy->getJoinColumnName($this->class->associationMappings[$idField]['joinColumns'][0],$this->class,$this->platform);$targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);$targetType = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em);if ($targetType === []) {throw UnrecognizedField::byFullyQualifiedName($this->class->name, $targetMapping->identifier[0]);}$types[] = reset($targetType);}if ($versioned) {$versionField = $this->class->versionField;assert($versionField !== null);$versionFieldType = $this->class->fieldMappings[$versionField]['type'];$versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform);$where[] = $versionColumn;$types[] = $this->class->fieldMappings[$versionField]['type'];$params[] = $this->class->reflFields[$versionField]->getValue($entity);switch ($versionFieldType) {case Types::SMALLINT:case Types::INTEGER:case Types::BIGINT:$set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';break;case Types::DATETIME_MUTABLE:$set[] = $versionColumn . ' = CURRENT_TIMESTAMP';break;}}$sql = 'UPDATE ' . $quotedTableName. ' SET ' . implode(', ', $set). ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';$result = $this->conn->executeStatement($sql, $params, $types);if ($versioned && ! $result) {throw OptimisticLockException::lockFailed($entity);}}/*** @param array<mixed> $identifier* @param string[] $types** @todo Add check for platform if it supports foreign keys/cascading.*/protected function deleteJoinTableRecords(array $identifier, array $types): void{foreach ($this->class->associationMappings as $mapping) {if ($mapping['type'] !== ClassMetadata::MANY_TO_MANY) {continue;}// @Todo this only covers scenarios with no inheritance or of the same level. Is there something// like self-referential relationship between different levels of an inheritance hierarchy? I hope not!$selfReferential = ($mapping['targetEntity'] === $mapping['sourceEntity']);$class = $this->class;$association = $mapping;$otherColumns = [];$otherKeys = [];$keys = [];if (! $mapping['isOwningSide']) {$class = $this->em->getClassMetadata($mapping['targetEntity']);$association = $class->associationMappings[$mapping['mappedBy']];}$joinColumns = $mapping['isOwningSide']? $association['joinTable']['joinColumns']: $association['joinTable']['inverseJoinColumns'];if ($selfReferential) {$otherColumns = ! $mapping['isOwningSide']? $association['joinTable']['joinColumns']: $association['joinTable']['inverseJoinColumns'];}foreach ($joinColumns as $joinColumn) {$keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);}foreach ($otherColumns as $joinColumn) {$otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);}if (isset($mapping['isOnDeleteCascade'])) {continue;}$joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);$this->conn->delete($joinTableName, array_combine($keys, $identifier), $types);if ($selfReferential) {$this->conn->delete($joinTableName, array_combine($otherKeys, $identifier), $types);}}}/*** {@inheritdoc}*/public function delete($entity){$class = $this->class;$identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);$tableName = $this->quoteStrategy->getTableName($class, $this->platform);$idColumns = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform);$id = array_combine($idColumns, $identifier);$types = $this->getClassIdentifiersTypes($class);$this->deleteJoinTableRecords($identifier, $types);return (bool) $this->conn->delete($tableName, $id, $types);}/*** Prepares the changeset of an entity for database insertion (UPDATE).** The changeset is obtained from the currently running UnitOfWork.** During this preparation the array that is passed as the second parameter is filled with* <columnName> => <value> pairs, grouped by table name.** Example:* <code>* array(* 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),* 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),* ...* )* </code>** @param object $entity The entity for which to prepare the data.* @param bool $isInsert Whether the data to be prepared refers to an insert statement.** @return mixed[][] The prepared data.* @psalm-return array<string, array<array-key, mixed|null>>*/protected function prepareUpdateData($entity, bool $isInsert = false){$versionField = null;$result = [];$uow = $this->em->getUnitOfWork();$versioned = $this->class->isVersioned;if ($versioned !== false) {$versionField = $this->class->versionField;}foreach ($uow->getEntityChangeSet($entity) as $field => $change) {if (isset($versionField) && $versionField === $field) {continue;}if (isset($this->class->embeddedClasses[$field])) {continue;}$newVal = $change[1];if (! isset($this->class->associationMappings[$field])) {$fieldMapping = $this->class->fieldMappings[$field];$columnName = $fieldMapping['columnName'];if (! $isInsert && isset($fieldMapping['notUpdatable'])) {continue;}if ($isInsert && isset($fieldMapping['notInsertable'])) {continue;}$this->columnTypes[$columnName] = $fieldMapping['type'];$result[$this->getOwningTable($field)][$columnName] = $newVal;continue;}$assoc = $this->class->associationMappings[$field];// Only owning side of x-1 associations can have a FK column.if (! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {continue;}if ($newVal !== null) {$oid = spl_object_id($newVal);if (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {// The associated entity $newVal is not yet persisted, so we must// set $newVal = null, in order to insert a null value and schedule an// extra update on the UnitOfWork.$uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);$newVal = null;}}$newValId = null;if ($newVal !== null) {$newValId = $uow->getEntityIdentifier($newVal);}$targetClass = $this->em->getClassMetadata($assoc['targetEntity']);$owningTable = $this->getOwningTable($field);foreach ($assoc['joinColumns'] as $joinColumn) {$sourceColumn = $joinColumn['name'];$targetColumn = $joinColumn['referencedColumnName'];$quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);$this->quotedColumns[$sourceColumn] = $quotedColumn;$this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);$result[$owningTable][$sourceColumn] = $newValId? $newValId[$targetClass->getFieldForColumn($targetColumn)]: null;}}return $result;}/*** Prepares the data changeset of a managed entity for database insertion (initial INSERT).* The changeset of the entity is obtained from the currently running UnitOfWork.** The default insert data preparation is the same as for updates.** @see prepareUpdateData** @param object $entity The entity for which to prepare the data.** @return mixed[][] The prepared data for the tables to update.* @psalm-return array<string, mixed[]>*/protected function prepareInsertData($entity){return $this->prepareUpdateData($entity, true);}/*** {@inheritdoc}*/public function getOwningTable($fieldName){return $this->class->getTableName();}/*** {@inheritdoc}*/public function load(array $criteria, $entity = null, $assoc = null, array $hints = [], $lockMode = null, $limit = null, ?array $orderBy = null){$this->switchPersisterContext(null, $limit);$sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);[$params, $types] = $this->expandParameters($criteria);$stmt = $this->conn->executeQuery($sql, $params, $types);if ($entity !== null) {$hints[Query::HINT_REFRESH] = true;$hints[Query::HINT_REFRESH_ENTITY] = $entity;}$hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);$entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);return $entities ? $entities[0] : null;}/*** {@inheritdoc}*/public function loadById(array $identifier, $entity = null){return $this->load($identifier, $entity);}/*** {@inheritdoc}*/public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = []){$foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity']);if ($foundEntity !== false) {return $foundEntity;}$targetClass = $this->em->getClassMetadata($assoc['targetEntity']);if ($assoc['isOwningSide']) {$isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']);// Mark inverse side as fetched in the hints, otherwise the UoW would// try to load it in a separate query (remember: to-one inverse sides can not be lazy).$hints = [];if ($isInverseSingleValued) {$hints['fetched']['r'][$assoc['inversedBy']] = true;}$targetEntity = $this->load($identifier, null, $assoc, $hints);// Complete bidirectional association, if necessaryif ($targetEntity !== null && $isInverseSingleValued) {$targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity);}return $targetEntity;}$sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);$owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);$computedIdentifier = [];// TRICKY: since the association is specular source and target are flippedforeach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {throw MappingException::joinColumnMustPointToMappedField($sourceClass->name,$sourceKeyColumn);}$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);}$targetEntity = $this->load($computedIdentifier, null, $assoc);if ($targetEntity !== null) {$targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);}return $targetEntity;}/*** {@inheritdoc}*/public function refresh(array $id, $entity, $lockMode = null){$sql = $this->getSelectSQL($id, null, $lockMode);[$params, $types] = $this->expandParameters($id);$stmt = $this->conn->executeQuery($sql, $params, $types);$hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);$hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);}/*** {@inheritDoc}*/public function count($criteria = []){$sql = $this->getCountSQL($criteria);[$params, $types] = $criteria instanceof Criteria? $this->expandCriteriaParameters($criteria): $this->expandParameters($criteria);return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();}/*** {@inheritdoc}*/public function loadCriteria(Criteria $criteria){$orderBy = $criteria->getOrderings();$limit = $criteria->getMaxResults();$offset = $criteria->getFirstResult();$query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);[$params, $types] = $this->expandCriteriaParameters($criteria);$stmt = $this->conn->executeQuery($query, $params, $types);$hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);}/*** {@inheritdoc}*/public function expandCriteriaParameters(Criteria $criteria){$expression = $criteria->getWhereExpression();$sqlParams = [];$sqlTypes = [];if ($expression === null) {return [$sqlParams, $sqlTypes];}$valueVisitor = new SqlValueVisitor();$valueVisitor->dispatch($expression);[$params, $types] = $valueVisitor->getParamsAndTypes();foreach ($params as $param) {$sqlParams = array_merge($sqlParams, $this->getValues($param));}foreach ($types as $type) {[$field, $value] = $type;$sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));}return [$sqlParams, $sqlTypes];}/*** {@inheritdoc}*/public function loadAll(array $criteria = [], ?array $orderBy = null, $limit = null, $offset = null){$this->switchPersisterContext($offset, $limit);$sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);[$params, $types] = $this->expandParameters($criteria);$stmt = $this->conn->executeQuery($sql, $params, $types);$hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);}/*** {@inheritdoc}*/public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null){$this->switchPersisterContext($offset, $limit);$stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);return $this->loadArrayFromResult($assoc, $stmt);}/*** Loads an array of entities from a given DBAL statement.** @param mixed[] $assoc** @return mixed[]*/private function loadArrayFromResult(array $assoc, Result $stmt): array{$rsm = $this->currentPersisterContext->rsm;$hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true];if (isset($assoc['indexBy'])) {$rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.$rsm->addIndexBy('r', $assoc['indexBy']);}return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);}/*** Hydrates a collection from a given DBAL statement.** @param mixed[] $assoc** @return mixed[]*/private function loadCollectionFromStatement(array $assoc,Result $stmt,PersistentCollection $coll): array {$rsm = $this->currentPersisterContext->rsm;$hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true,'collection' => $coll,];if (isset($assoc['indexBy'])) {$rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.$rsm->addIndexBy('r', $assoc['indexBy']);}return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);}/*** {@inheritdoc}*/public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection){$stmt = $this->getManyToManyStatement($assoc, $sourceEntity);return $this->loadCollectionFromStatement($assoc, $stmt, $collection);}/*** @param object $sourceEntity* @psalm-param array<string, mixed> $assoc** @return Result** @throws MappingException*/private function getManyToManyStatement(array $assoc,$sourceEntity,?int $offset = null,?int $limit = null) {$this->switchPersisterContext($offset, $limit);$sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);$class = $sourceClass;$association = $assoc;$criteria = [];$parameters = [];if (! $assoc['isOwningSide']) {$class = $this->em->getClassMetadata($assoc['targetEntity']);$association = $class->associationMappings[$assoc['mappedBy']];}$joinColumns = $assoc['isOwningSide']? $association['joinTable']['joinColumns']: $association['joinTable']['inverseJoinColumns'];$quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);foreach ($joinColumns as $joinColumn) {$sourceKeyColumn = $joinColumn['referencedColumnName'];$quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);switch (true) {case $sourceClass->containsForeignIdentifier:$field = $sourceClass->getFieldForColumn($sourceKeyColumn);$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);if (isset($sourceClass->associationMappings[$field])) {$value = $this->em->getUnitOfWork()->getEntityIdentifier($value);$value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];}break;case isset($sourceClass->fieldNames[$sourceKeyColumn]):$field = $sourceClass->fieldNames[$sourceKeyColumn];$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);break;default:throw MappingException::joinColumnMustPointToMappedField($sourceClass->name,$sourceKeyColumn);}$criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value;$parameters[] = ['value' => $value,'field' => $field,'class' => $sourceClass,];}$sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);[$params, $types] = $this->expandToManyParameters($parameters);return $this->conn->executeQuery($sql, $params, $types);}/*** {@inheritdoc}*/public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit = null, $offset = null, ?array $orderBy = null){$this->switchPersisterContext($offset, $limit);$lockSql = '';$joinSql = '';$orderBySql = '';if ($assoc !== null && $assoc['type'] === ClassMetadata::MANY_TO_MANY) {$joinSql = $this->getSelectManyToManyJoinSQL($assoc);}if (isset($assoc['orderBy'])) {$orderBy = $assoc['orderBy'];}if ($orderBy) {$orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name));}$conditionSql = $criteria instanceof Criteria? $this->getSelectConditionCriteriaSQL($criteria): $this->getSelectConditionSQL($criteria, $assoc);switch ($lockMode) {case LockMode::PESSIMISTIC_READ:$lockSql = ' ' . $this->platform->getReadLockSQL();break;case LockMode::PESSIMISTIC_WRITE:$lockSql = ' ' . $this->platform->getWriteLockSQL();break;}$columnList = $this->getSelectColumnsSQL();$tableAlias = $this->getSQLTableAlias($this->class->name);$filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);$tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);if ($filterSql !== '') {$conditionSql = $conditionSql? $conditionSql . ' AND ' . $filterSql: $filterSql;}$select = 'SELECT ' . $columnList;$from = ' FROM ' . $tableName . ' ' . $tableAlias;$join = $this->currentPersisterContext->selectJoinSql . $joinSql;$where = ($conditionSql ? ' WHERE ' . $conditionSql : '');$lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE);$query = $select. $lock. $join. $where. $orderBySql;return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql;}/*** {@inheritDoc}*/public function getCountSQL($criteria = []){$tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);$tableAlias = $this->getSQLTableAlias($this->class->name);$conditionSql = $criteria instanceof Criteria? $this->getSelectConditionCriteriaSQL($criteria): $this->getSelectConditionSQL($criteria);$filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);if ($filterSql !== '') {$conditionSql = $conditionSql? $conditionSql . ' AND ' . $filterSql: $filterSql;}return 'SELECT COUNT(*) '. 'FROM ' . $tableName . ' ' . $tableAlias. (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);}/*** Gets the ORDER BY SQL snippet for ordered collections.** @psalm-param array<string, string> $orderBy** @throws InvalidOrientation* @throws InvalidFindByCall* @throws UnrecognizedField*/final protected function getOrderBySQL(array $orderBy, string $baseTableAlias): string{$orderByList = [];foreach ($orderBy as $fieldName => $orientation) {$orientation = strtoupper(trim($orientation));if ($orientation !== 'ASC' && $orientation !== 'DESC') {throw InvalidOrientation::fromClassNameAndField($this->class->name, $fieldName);}if (isset($this->class->fieldMappings[$fieldName])) {$tableAlias = isset($this->class->fieldMappings[$fieldName]['inherited'])? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]['inherited']): $baseTableAlias;$columnName = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);$orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;continue;}if (isset($this->class->associationMappings[$fieldName])) {if (! $this->class->associationMappings[$fieldName]['isOwningSide']) {throw InvalidFindByCall::fromInverseSideUsage($this->class->name, $fieldName);}$tableAlias = isset($this->class->associationMappings[$fieldName]['inherited'])? $this->getSQLTableAlias($this->class->associationMappings[$fieldName]['inherited']): $baseTableAlias;foreach ($this->class->associationMappings[$fieldName]['joinColumns'] as $joinColumn) {$columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);$orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;}continue;}throw UnrecognizedField::byFullyQualifiedName($this->class->name, $fieldName);}return ' ORDER BY ' . implode(', ', $orderByList);}/*** Gets the SQL fragment with the list of columns to select when querying for* an entity in this persister.** Subclasses should override this method to alter or change the select column* list SQL fragment. Note that in the implementation of BasicEntityPersister* the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.* Subclasses may or may not do the same.** @return string The SQL fragment.*/protected function getSelectColumnsSQL(){if ($this->currentPersisterContext->selectColumnListSql !== null) {return $this->currentPersisterContext->selectColumnListSql;}$columnList = [];$this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root// Add regular columns to select listforeach ($this->class->fieldNames as $field) {$columnList[] = $this->getSelectColumnSQL($field, $this->class);}$this->currentPersisterContext->selectJoinSql = '';$eagerAliasCounter = 0;foreach ($this->class->associationMappings as $assocField => $assoc) {$assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class);if ($assocColumnSQL) {$columnList[] = $assocColumnSQL;}$isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];$isAssocFromOneEager = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {continue;}if ((($assoc['type'] & ClassMetadata::TO_MANY) > 0) && $this->currentPersisterContext->handlesLimits) {continue;}$eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);if ($eagerEntity->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {continue; // now this is why you shouldn't use inheritance}$assocAlias = 'e' . ($eagerAliasCounter++);$this->currentPersisterContext->rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);foreach ($eagerEntity->fieldNames as $field) {$columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias);}foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) {$eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL($eagerAssocField,$eagerAssoc,$eagerEntity,$assocAlias);if ($eagerAssocColumnSQL) {$columnList[] = $eagerAssocColumnSQL;}}$association = $assoc;$joinCondition = [];if (isset($assoc['indexBy'])) {$this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc['indexBy']);}if (! $assoc['isOwningSide']) {$eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);$association = $eagerEntity->getAssociationMapping($assoc['mappedBy']);}$joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias);$joinTableName = $this->quoteStrategy->getTableName($eagerEntity, $this->platform);if ($assoc['isOwningSide']) {$tableAlias = $this->getSQLTableAlias($association['targetEntity'], $assocAlias);$this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association['joinColumns']);foreach ($association['joinColumns'] as $joinColumn) {$sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);$targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);$joinCondition[] = $this->getSQLTableAlias($association['sourceEntity']). '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol;}// Add filter SQL$filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias);if ($filterSql) {$joinCondition[] = $filterSql;}} else {$this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN';foreach ($association['joinColumns'] as $joinColumn) {$sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);$targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);$joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '. $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol;}}$this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';$this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);}$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);return $this->currentPersisterContext->selectColumnListSql;}/*** Gets the SQL join fragment used when selecting entities from an association.** @param string $field* @param mixed[] $assoc* @param string $alias** @return string*/protected function getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r'){if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {return '';}$columnList = [];$targetClass = $this->em->getClassMetadata($assoc['targetEntity']);$isIdentifier = isset($assoc['id']) && $assoc['id'] === true;$sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias === 'r' ? '' : $alias));foreach ($assoc['joinColumns'] as $joinColumn) {$quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);$resultColumnName = $this->getSQLColumnAlias($joinColumn['name']);$type = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);$this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn['name'], $isIdentifier, $type);$columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName);}return implode(', ', $columnList);}/*** Gets the SQL join fragment used when selecting entities from a* many-to-many association.** @psalm-param array<string, mixed> $manyToMany** @return string*/protected function getSelectManyToManyJoinSQL(array $manyToMany){$conditions = [];$association = $manyToMany;$sourceTableAlias = $this->getSQLTableAlias($this->class->name);if (! $manyToMany['isOwningSide']) {$targetEntity = $this->em->getClassMetadata($manyToMany['targetEntity']);$association = $targetEntity->associationMappings[$manyToMany['mappedBy']];}$joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);$joinColumns = $manyToMany['isOwningSide']? $association['joinTable']['inverseJoinColumns']: $association['joinTable']['joinColumns'];foreach ($joinColumns as $joinColumn) {$quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);$quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);$conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn;}return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);}/*** {@inheritdoc}*/public function getInsertSQL(){if ($this->insertSql !== null) {return $this->insertSql;}$columns = $this->getInsertColumnList();$tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);if (empty($columns)) {$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);$this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);return $this->insertSql;}$values = [];$columns = array_unique($columns);foreach ($columns as $column) {$placeholder = '?';if (isset($this->class->fieldNames[$column])&& isset($this->columnTypes[$this->class->fieldNames[$column]])&& isset($this->class->fieldMappings[$this->class->fieldNames[$column]]['requireSQLConversion'])) {$type = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]);$placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);}$values[] = $placeholder;}$columns = implode(', ', $columns);$values = implode(', ', $values);$this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);return $this->insertSql;}/*** Gets the list of columns to put in the INSERT SQL statement.** Subclasses should override this method to alter or change the list of* columns placed in the INSERT statements used by the persister.** @return string[] The list of columns.* @psalm-return list<string>*/protected function getInsertColumnList(){$columns = [];foreach ($this->class->reflFields as $name => $field) {if ($this->class->isVersioned && $this->class->versionField === $name) {continue;}if (isset($this->class->embeddedClasses[$name])) {continue;}if (isset($this->class->associationMappings[$name])) {$assoc = $this->class->associationMappings[$name];if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {foreach ($assoc['joinColumns'] as $joinColumn) {$columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);}}continue;}if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) {if (isset($this->class->fieldMappings[$name]['notInsertable'])) {continue;}$columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);$this->columnTypes[$name] = $this->class->fieldMappings[$name]['type'];}}return $columns;}/*** Gets the SQL snippet of a qualified column name for the given field name.** @param string $field The field name.* @param ClassMetadata $class The class that declares this field. The table this class is* mapped to must own the column for the given field.* @param string $alias** @return string*/protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r'){$root = $alias === 'r' ? '' : $alias;$tableAlias = $this->getSQLTableAlias($class->name, $root);$fieldMapping = $class->fieldMappings[$field];$sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));$columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']);$this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);if (! empty($fieldMapping['enumType'])) {$this->currentPersisterContext->rsm->addEnumResult($columnAlias, $fieldMapping['enumType']);}if (isset($fieldMapping['requireSQLConversion'])) {$type = Type::getType($fieldMapping['type']);$sql = $type->convertToPHPValueSQL($sql, $this->platform);}return $sql . ' AS ' . $columnAlias;}/*** Gets the SQL table alias for the given class name.** @param string $className* @param string $assocName** @return string The SQL table alias.** @todo Reconsider. Binding table aliases to class names is not such a good idea.*/protected function getSQLTableAlias($className, $assocName = ''){if ($assocName) {$className .= '#' . $assocName;}if (isset($this->currentPersisterContext->sqlTableAliases[$className])) {return $this->currentPersisterContext->sqlTableAliases[$className];}$tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;$this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias;return $tableAlias;}/*** {@inheritdoc}*/public function lock(array $criteria, $lockMode){$lockSql = '';$conditionSql = $this->getSelectConditionSQL($criteria);switch ($lockMode) {case LockMode::PESSIMISTIC_READ:$lockSql = $this->platform->getReadLockSQL();break;case LockMode::PESSIMISTIC_WRITE:$lockSql = $this->platform->getWriteLockSQL();break;}$lock = $this->getLockTablesSql($lockMode);$where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';$sql = 'SELECT 1 '. $lock. $where. $lockSql;[$params, $types] = $this->expandParameters($criteria);$this->conn->executeQuery($sql, $params, $types);}/*** Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.** @param int|null $lockMode One of the Doctrine\DBAL\LockMode::* constants.* @psalm-param LockMode::*|null $lockMode** @return string*/protected function getLockTablesSql($lockMode){if ($lockMode === null) {Deprecation::trigger('doctrine/orm','https://github.com/doctrine/orm/pull/9466','Passing null as argument to %s is deprecated, pass LockMode::NONE instead.',__METHOD__);$lockMode = LockMode::NONE;}return $this->platform->appendLockHint('FROM '. $this->quoteStrategy->getTableName($this->class, $this->platform) . ' '. $this->getSQLTableAlias($this->class->name),$lockMode);}/*** Gets the Select Where Condition from a Criteria object.** @return string*/protected function getSelectConditionCriteriaSQL(Criteria $criteria){$expression = $criteria->getWhereExpression();if ($expression === null) {return '';}$visitor = new SqlExpressionVisitor($this, $this->class);return $visitor->dispatch($expression);}/*** {@inheritdoc}*/public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null){$selectedColumns = [];$columns = $this->getSelectConditionStatementColumnSQL($field, $assoc);if (count($columns) > 1 && $comparison === Comparison::IN) {/** @todo try to support multi-column IN expressions.* Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B'))*/throw CantUseInOperatorOnCompositeKeys::create();}foreach ($columns as $column) {$placeholder = '?';if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {$type = Type::getType($this->class->fieldMappings[$field]['type']);$placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform);}if ($comparison !== null) {// special case null value handlingif (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) {$selectedColumns[] = $column . ' IS NULL';continue;}if ($comparison === Comparison::NEQ && $value === null) {$selectedColumns[] = $column . ' IS NOT NULL';continue;}$selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);continue;}if (is_array($value)) {$in = sprintf('%s IN (%s)', $column, $placeholder);if (array_search(null, $value, true) !== false) {$selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);continue;}$selectedColumns[] = $in;continue;}if ($value === null) {$selectedColumns[] = sprintf('%s IS NULL', $column);continue;}$selectedColumns[] = sprintf('%s = %s', $column, $placeholder);}return implode(' AND ', $selectedColumns);}/*** Builds the left-hand-side of a where condition statement.** @psalm-param array<string, mixed>|null $assoc** @return string[]* @psalm-return list<string>** @throws InvalidFindByCall* @throws UnrecognizedField*/private function getSelectConditionStatementColumnSQL(string $field,?array $assoc = null): array {if (isset($this->class->fieldMappings[$field])) {$className = $this->class->fieldMappings[$field]['inherited'] ?? $this->class->name;return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)];}if (isset($this->class->associationMappings[$field])) {$association = $this->class->associationMappings[$field];// Many-To-Many requires join table check for joinColumn$columns = [];$class = $this->class;if ($association['type'] === ClassMetadata::MANY_TO_MANY) {if (! $association['isOwningSide']) {$association = $assoc;}$joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);$joinColumns = $assoc['isOwningSide']? $association['joinTable']['joinColumns']: $association['joinTable']['inverseJoinColumns'];foreach ($joinColumns as $joinColumn) {$columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);}} else {if (! $association['isOwningSide']) {throw InvalidFindByCall::fromInverseSideUsage($this->class->name,$field);}$className = $association['inherited'] ?? $this->class->name;foreach ($association['joinColumns'] as $joinColumn) {$columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);}}return $columns;}if ($assoc !== null && ! str_contains($field, ' ') && ! str_contains($field, '(')) {// very careless developers could potentially open up this normally hidden api for userland attacks,// therefore checking for spaces and function calls which are not allowed.// found a join column condition, not really a "field"return [$field];}throw UnrecognizedField::byFullyQualifiedName($this->class->name, $field);}/*** Gets the conditional SQL fragment used in the WHERE clause when selecting* entities in this persister.** Subclasses are supposed to override this method if they intend to change* or alter the criteria by which entities are selected.** @param mixed[]|null $assoc* @psalm-param array<string, mixed> $criteria* @psalm-param array<string, mixed>|null $assoc** @return string*/protected function getSelectConditionSQL(array $criteria, $assoc = null){$conditions = [];foreach ($criteria as $field => $value) {$conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc);}return implode(' AND ', $conditions);}/*** {@inheritdoc}*/public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null){$this->switchPersisterContext($offset, $limit);$stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);return $this->loadArrayFromResult($assoc, $stmt);}/*** {@inheritdoc}*/public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection){$stmt = $this->getOneToManyStatement($assoc, $sourceEntity);return $this->loadCollectionFromStatement($assoc, $stmt, $collection);}/*** Builds criteria and execute SQL statement to fetch the one to many entities from.** @param object $sourceEntity* @psalm-param array<string, mixed> $assoc*/private function getOneToManyStatement(array $assoc,$sourceEntity,?int $offset = null,?int $limit = null): Result {$this->switchPersisterContext($offset, $limit);$criteria = [];$parameters = [];$owningAssoc = $this->class->associationMappings[$assoc['mappedBy']];$sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);$tableAlias = $this->getSQLTableAlias($owningAssoc['inherited'] ?? $this->class->name);foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {if ($sourceClass->containsForeignIdentifier) {$field = $sourceClass->getFieldForColumn($sourceKeyColumn);$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);if (isset($sourceClass->associationMappings[$field])) {$value = $this->em->getUnitOfWork()->getEntityIdentifier($value);$value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];}$criteria[$tableAlias . '.' . $targetKeyColumn] = $value;$parameters[] = ['value' => $value,'field' => $field,'class' => $sourceClass,];continue;}$field = $sourceClass->fieldNames[$sourceKeyColumn];$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);$criteria[$tableAlias . '.' . $targetKeyColumn] = $value;$parameters[] = ['value' => $value,'field' => $field,'class' => $sourceClass,];}$sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);[$params, $types] = $this->expandToManyParameters($parameters);return $this->conn->executeQuery($sql, $params, $types);}/*** {@inheritdoc}*/public function expandParameters($criteria){$params = [];$types = [];foreach ($criteria as $field => $value) {if ($value === null) {continue; // skip null values.}$types = array_merge($types, $this->getTypes($field, $value, $this->class));$params = array_merge($params, $this->getValues($value));}return [$params, $types];}/*** Expands the parameters from the given criteria and use the correct binding types if found,* specialized for OneToMany or ManyToMany associations.** @param mixed[][] $criteria an array of arrays containing following:* - field to which each criterion will be bound* - value to be bound* - class to which the field belongs to** @return mixed[][]* @psalm-return array{0: array, 1: list<int|string|null>}*/private function expandToManyParameters(array $criteria): array{$params = [];$types = [];foreach ($criteria as $criterion) {if ($criterion['value'] === null) {continue; // skip null values.}$types = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));$params = array_merge($params, $this->getValues($criterion['value']));}return [$params, $types];}/*** Infers field types to be used by parameter type casting.** @param mixed $value** @return int[]|null[]|string[]* @psalm-return list<int|string|null>** @throws QueryException*/private function getTypes(string $field, $value, ClassMetadata $class): array{$types = [];switch (true) {case isset($class->fieldMappings[$field]):$types = array_merge($types, [$class->fieldMappings[$field]['type']]);break;case isset($class->associationMappings[$field]):$assoc = $class->associationMappings[$field];$class = $this->em->getClassMetadata($assoc['targetEntity']);if (! $assoc['isOwningSide']) {$assoc = $class->associationMappings[$assoc['mappedBy']];$class = $this->em->getClassMetadata($assoc['targetEntity']);}$columns = $assoc['type'] === ClassMetadata::MANY_TO_MANY? $assoc['relationToTargetKeyColumns']: $assoc['sourceToTargetKeyColumns'];foreach ($columns as $column) {$types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);}break;default:$types[] = null;break;}if (is_array($value)) {return array_map(static function ($type) {$type = Type::getType($type);return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;}, $types);}return $types;}/*** Retrieves the parameters that identifies a value.** @param mixed $value** @return mixed[]*/private function getValues($value): array{if (is_array($value)) {$newValue = [];foreach ($value as $itemValue) {$newValue = array_merge($newValue, $this->getValues($itemValue));}return [$newValue];}return $this->getIndividualValue($value);}/*** Retrieves an individual parameter value.** @param mixed $value** @psalm-return list<mixed>*/private function getIndividualValue($value): array{if (! is_object($value)) {return [$value];}if ($value instanceof BackedEnum) {return [$value->value];}$valueClass = ClassUtils::getClass($value);if ($this->em->getMetadataFactory()->isTransient($valueClass)) {return [$value];}$class = $this->em->getClassMetadata($valueClass);if ($class->isIdentifierComposite) {$newValue = [];foreach ($class->getIdentifierValues($value) as $innerValue) {$newValue = array_merge($newValue, $this->getValues($innerValue));}return $newValue;}return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)];}/*** {@inheritdoc}*/public function exists($entity, ?Criteria $extraConditions = null){$criteria = $this->class->getIdentifierValues($entity);if (! $criteria) {return false;}$alias = $this->getSQLTableAlias($this->class->name);$sql = 'SELECT 1 '. $this->getLockTablesSql(LockMode::NONE). ' WHERE ' . $this->getSelectConditionSQL($criteria);[$params, $types] = $this->expandParameters($criteria);if ($extraConditions !== null) {$sql .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);[$criteriaParams, $criteriaTypes] = $this->expandCriteriaParameters($extraConditions);$params = array_merge($params, $criteriaParams);$types = array_merge($types, $criteriaTypes);}$filterSql = $this->generateFilterConditionSQL($this->class, $alias);if ($filterSql) {$sql .= ' AND ' . $filterSql;}return (bool) $this->conn->fetchOne($sql, $params, $types);}/*** Generates the appropriate join SQL for the given join column.** @param array[] $joinColumns The join columns definition of an association.* @psalm-param array<array<string, mixed>> $joinColumns** @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.*/protected function getJoinSQLForJoinColumns($joinColumns){// if one of the join columns is nullable, return left joinforeach ($joinColumns as $joinColumn) {if (! isset($joinColumn['nullable']) || $joinColumn['nullable']) {return 'LEFT JOIN';}}return 'INNER JOIN';}/*** @param string $columnName** @return string*/public function getSQLColumnAlias($columnName){return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform);}/*** Generates the filter SQL for a given entity and table alias.** @param ClassMetadata $targetEntity Metadata of the target entity.* @param string $targetTableAlias The table alias of the joined/selected table.** @return string The SQL query part to add to a query.*/protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias){$filterClauses = [];foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {$filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);if ($filterExpr !== '') {$filterClauses[] = '(' . $filterExpr . ')';}}$sql = implode(' AND ', $filterClauses);return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL"}/*** Switches persister context according to current query offset/limits** This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved** @param int|null $offset* @param int|null $limit** @return void*/protected function switchPersisterContext($offset, $limit){if ($offset === null && $limit === null) {$this->currentPersisterContext = $this->noLimitsContext;return;}$this->currentPersisterContext = $this->limitsHandlingContext;}/*** @return string[]* @psalm-return list<string>*/protected function getClassIdentifiersTypes(ClassMetadata $class): array{$entityManager = $this->em;return array_map(static function ($fieldName) use ($class, $entityManager): string {$types = PersisterHelper::getTypeOfField($fieldName, $class, $entityManager);assert(isset($types[0]));return $types[0];},$class->identifier);}}