diff --git a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php index 8e31bcf94..7e859ce8d 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php +++ b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php @@ -30,6 +30,39 @@ use Doctrine\ORM\Mapping\ClassMetadataInfo; */ class ResultSetMappingBuilder extends ResultSetMapping { + /** + * Picking this rename mode will register entity columns as is, + * as they are in the database. This can cause clashes when multiple + * entities are fetched that have columns with the same name. + * + * @var int + */ + const COLUMN_RENAMING_NONE = 1; + + /** + * Picking custom renaming allows the user to define the renaming + * of specific columns with a rename array that contains column names as + * keys and result alias as values. + * + * @var int + */ + const COLUMN_RENAMING_CUSTOM = 2; + + /** + * Incremental renaming uses a result set mapping internal counter to add a + * number to each column result, leading to uniqueness. This only works if + * you use {@see generateSelectClause()} to generate the SELECT clause for + * you. + * + * @var int + */ + const COLUMN_RENAMING_INCREMENT = 3; + + /** + * @var int + */ + private $sqlCounter = 0; + /** * @var EntityManager */ @@ -48,12 +81,15 @@ class ResultSetMappingBuilder extends ResultSetMapping * * @param string $class The class name of the root entity. * @param string $alias The unique alias to use for the root entity. + * @param int|array $renameMode One of the COLUMN_RENAMING_* constants or array for BC reasons (CUSTOM). * @param array $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName) */ - public function addRootEntityFromClassMetadata($class, $alias, $renamedColumns = array()) + public function addRootEntityFromClassMetadata($class, $alias, $renameMode = self::COLUMN_RENAMING_NONE, $renamedColumns = array()) { + $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); + $this->addEntityResult($class, $alias); - $this->addAllClassFields($class, $alias, $renamedColumns); + $this->addAllClassFields($class, $alias, $columnAliasMap); } /** @@ -63,50 +99,122 @@ class ResultSetMappingBuilder extends ResultSetMapping * @param string $alias The unique alias to use for the joined entity. * @param string $parentAlias The alias of the entity result that is the parent of this joined result. * @param object $relation The association field that connects the parent entity result with the joined entity result. + * @param int|array $renameMode * @param array $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName) */ - public function addJoinedEntityFromClassMetadata($class, $alias, $parentAlias, $relation, $renamedColumns = array()) + public function addJoinedEntityFromClassMetadata($class, $alias, $parentAlias, $relation, $renameMode = self::COLUMN_RENAMING_NONE, $renamedColumns = array()) { + $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); + $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation); - $this->addAllClassFields($class, $alias, $renamedColumns); + $this->addAllClassFields($class, $alias, $columnAliasMap); } /** * Adds all fields of the given class to the result set mapping (columns and meta fields) */ - protected function addAllClassFields($class, $alias, $renamedColumns = array()) + protected function addAllClassFields($class, $alias, $columnAliasMap = array()) { $classMetadata = $this->em->getClassMetadata($class); + $platform = $this->em->getConnection()->getDatabasePlatform(); + if ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined()) { throw new \InvalidArgumentException('ResultSetMapping builder does not currently support inheritance.'); } - $platform = $this->em->getConnection()->getDatabasePlatform(); + + foreach ($classMetadata->getColumnNames() as $columnName) { $propertyName = $classMetadata->getFieldName($columnName); - if (isset($renamedColumns[$columnName])) { - $columnName = $renamedColumns[$columnName]; - } - $columnName = $platform->getSQLResultCasing($columnName); - if (isset($this->fieldMappings[$columnName])) { + $columnAlias = $platform->getSQLResultCasing($columnAliasMap[$columnName]); + + if (isset($this->fieldMappings[$columnAlias])) { throw new \InvalidArgumentException("The column '$columnName' conflicts with another column in the mapper."); } - $this->addFieldResult($alias, $columnName, $propertyName); + + $this->addFieldResult($alias, $columnAlias, $propertyName); } + foreach ($classMetadata->associationMappings as $associationMapping) { if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) { foreach ($associationMapping['joinColumns'] as $joinColumn) { - $columnName = $joinColumn['name']; - $renamedColumnName = isset($renamedColumns[$columnName]) ? $renamedColumns[$columnName] : $columnName; - $renamedColumnName = $platform->getSQLResultCasing($renamedColumnName); - if (isset($this->metaMappings[$renamedColumnName])) { - throw new \InvalidArgumentException("The column '$renamedColumnName' conflicts with another column in the mapper."); + $columnName = $joinColumn['name']; + $columnAlias = $platform->getSQLResultCasing($columnAliasMap[$columnName]); + + if (isset($this->metaMappings[$columnAlias])) { + throw new \InvalidArgumentException("The column '$columnAlias' conflicts with another column in the mapper."); } - $this->addMetaResult($alias, $renamedColumnName, $columnName); + + $this->addMetaResult($alias, $columnAlias, $columnName); } } } } + /** + * Get column alias for a given column. + * + * @param string $columnName + * @param int $mode + * @param array $customRenameColumns + * + * @return string + */ + private function getColumnAlias($columnName, $mode, array $customRenameColumns) + { + switch ($mode) { + case self::COLUMN_RENAMING_INCREMENT: + return $columnName . $this->sqlCounter++; + + case self::COLUMN_RENAMING_CUSTOM: + return isset($customRenameColumns[$columnName]) + ? $customRenameColumns[$columnName] : $columnName; + + case self::COLUMN_RENAMING_NONE: + return $columnName; + + } + } + + /** + * Retrieve a class columns and join columns aliases that are used + * in the SELECT clause. + * + * This depends on the renaming mode selected by the user. + * + * @param string $className + * @param int|array $mode + * @param array $customRenameColumns + * + * @return array + */ + private function getColumnAliasMap($className, $mode, array $customRenameColumns) + { + if (is_array($mode)) { + $customRenameColumns = $mode; + $mode = self::COLUMN_RENAMING_CUSTOM; + } + + $columnAlias = array(); + $class = $this->em->getClassMetadata($className); + + foreach ($class->getColumnNames() as $columnName) { + $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); + } + + foreach ($class->associationMappings as $associationMapping) { + if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) { + foreach ($associationMapping['joinColumns'] as $joinColumn) { + $columnName = $joinColumn['name']; + $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); + } + } + } + + + return $columnAlias; + } + + /** * Adds the mappings of the results of native SQL queries to the result set. @@ -261,4 +369,46 @@ class ResultSetMappingBuilder extends ResultSetMapping return $this; } + + /** + * Generate the Select clause from this ResultSetMappingBuilder + * + * Works only for all the entity results. The select parts for scalar + * expressions have to be written manually. + * + * @return string + */ + public function generateSelectClause($tableAliases = array()) + { + $sql = ""; + + foreach ($this->columnOwnerMap as $columnName => $dqlAlias) { + $tableAlias = isset($tableAliases[$dqlAlias]) + ? $tableAliases[$dqlAlias] : $dqlAlias; + + if ($sql) { + $sql .= ", "; + } + + $sql .= $tableAlias . "."; + + if (isset($this->fieldMappings[$columnName])) { + $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]); + $sql .= $class->fieldMappings[$this->fieldMappings[$columnName]]['columnName']; + } else if (isset($this->metaMappings[$columnName])) { + $sql .= $this->metaMappings[$columnName]; + } else if (isset($this->discriminatorColumn[$columnName])) { + $sql .= $this->discriminatorColumn[$columnName]; + } + + $sql .= " AS " . $columnName; + } + + return $sql; + } + + public function __toString() + { + return $this->generateSelectClause(array()); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php index 3436a74f4..669b378c1 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php @@ -709,4 +709,69 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase } -} \ No newline at end of file + /** + * @group DDC-2055 + */ + public function testGenerateSelectClauseNoRenameSingleEntity() + { + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + + $selectClause = $rsm->generateSelectClause(); + + $this->assertEquals('u.id AS id, u.status AS status, u.username AS username, u.name AS name, u.email_id AS email_id', $selectClause); + } + + /** + * @group DDC-2055 + */ + public function testGenerateSelectClauseCustomRenames() + { + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u', array( + 'id' => 'id1', + 'username' => 'username2' + )); + + $selectClause = $rsm->generateSelectClause(); + + $this->assertEquals('u.id AS id1, u.status AS status, u.username AS username2, u.name AS name, u.email_id AS email_id', $selectClause); + } + + /** + * @group DDC-2055 + */ + public function testGenerateSelectClauseRenameTableAlias() + { + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + + $selectClause = $rsm->generateSelectClause(array('u' => 'u1')); + + $this->assertEquals('u1.id AS id, u1.status AS status, u1.username AS username, u1.name AS name, u1.email_id AS email_id', $selectClause); + } + + /** + * @group DDC-2055 + */ + public function testGenerateSelectClauseIncrement() + { + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u', ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT); + + $selectClause = $rsm->generateSelectClause(); + + $this->assertEquals('u.id AS id0, u.status AS status1, u.username AS username2, u.name AS name3, u.email_id AS email_id4', $selectClause); + } + + /** + * @group DDC-2055 + */ + public function testGenerateSelectClauseToString() + { + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u', ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT); + + $this->assertEquals('u.id AS id0, u.status AS status1, u.username AS username2, u.name AS name3, u.email_id AS email_id4', (string)$rsm); + } +}