nList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName); } return implode(', ', $columnList); } 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); } 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; } 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; } 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; } 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; } 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); } 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); } protected function getSelectConditionCriteriaSQL(Criteria $criteria) { $expression = $criteria->getWhereExpression(); if ($expression === null) { return ''; } $visitor = new SqlExpressionVisitor($this, $this->class); return $visitor->dispatch($expression); } public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null) { $selectedColumns = []; $columns = $this->getSelectConditionStatementColumnSQL($field, $assoc); if (count($columns) > 1 && $comparison === Comparison::IN) { 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 handling if (($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); } 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); } protected function getSelectConditionSQL(array $criteria, $assoc = null) { $conditions = []; foreach ($criteria as $field => $value) { $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc); } return implode(' AND ', $conditions); } 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); } public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection) { $stmt = $this->getOneToManyStatement($assoc, $sourceEntity); return $this->loadCollectionFromStatement($assoc, $stmt, $collection); } 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); } 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]; } 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]; } 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; } 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); } 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)]; } 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); } protected function getJoinSQLForJoinColumns($joinColumns) { // if one of the join columns is nullable, return left join foreach ($joinColumns as $joinColumn) { if (!isset($joinColumn['nullable']) || $joinColumn['nullable']) { return 'LEFT JOIN'; } } return 'INNER JOIN'; } public function getSQLColumnAlias($columnName) { return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform); } 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" } protected function switchPersisterContext($offset, $limit) { if ($offset === null && $limit === null) { $this->currentPersisterContext = $this->noLimitsContext; return; } $this->currentPersisterContext = $this->limitsHandlingContext; } 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); } }