From 2b996128af1359b4ba2f0297c8692e7fa8c6de12 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Sun, 26 Feb 2012 00:16:54 -0300 Subject: [PATCH] sql result set mapping metadata --- .../ORM/Mapping/ClassMetadataInfo.php | 108 +++++++++++- lib/Doctrine/ORM/Mapping/ColumnResult.php | 42 +++++ lib/Doctrine/ORM/Mapping/MappingException.php | 22 ++- .../ORM/Mapping/NamedNativeQueries.php | 2 +- lib/Doctrine/ORM/Mapping/NamedNativeQuery.php | 2 +- .../ORM/Mapping/SqlResultSetMappings.php | 2 +- .../Tests/ORM/Mapping/ClassMetadataTest.php | 165 +++++++++++++++++- 7 files changed, 325 insertions(+), 18 deletions(-) create mode 100644 lib/Doctrine/ORM/Mapping/ColumnResult.php diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index eb5b014bc..3c4796307 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -262,15 +262,29 @@ class ClassMetadataInfo implements ClassMetadata * A native SQL named query definition has the following structure: *
      * array(
-     *     'name'                => ,
-     *     'query'               => ,
-     *     'resultClass'         => ,
-     *     'resultSetMapping'    => 
+     *     'name'               => ,
+     *     'query'              => ,
+     *     'resultClass'        => ,
+     *     'resultSetMapping'   => 
      * )
      * 
*/ public $namedNativeQueries = array(); + /** + * READ-ONLY: The mappings of the results of native SQL queries. + * + * A native result mapping definition has the following structure: + *
+     * array(
+     *     'name'               => ,
+     *     'entities'           => array(),
+     *     'columns'            => array()
+     * )
+     * 
+ */ + public $sqlResultSetMappings = array(); + /** * READ-ONLY: The field names of all fields that are part of the identifier/primary key * of the mapped entity class. @@ -782,6 +796,14 @@ class ClassMetadataInfo implements ClassMetadata $serialized[] = 'namedQueries'; } + if ($this->namedNativeQueries) { + $serialized[] = 'namedNativeQueries'; + } + + if ($this->sqlResultSetMappings) { + $serialized[] = 'sqlResultSetMappings'; + } + if ($this->isReadOnly) { $serialized[] = 'isReadOnly'; } @@ -1112,6 +1134,33 @@ class ClassMetadataInfo implements ClassMetadata return $this->namedNativeQueries; } + /** + * Gets the result set mapping. + * + * @see ClassMetadataInfo::$sqlResultSetMappings + * @throws MappingException + * @param string $name The result set mapping name + * @return array + */ + public function getSqlResultSetMapping($name) + { + if ( ! isset($this->sqlResultSetMappings[$name])) { + throw MappingException::resultMappingNotFound($this->name, $name); + } + + return $this->sqlResultSetMappings[$name]; + } + + /** + * Gets all sql result set mappings of the class. + * + * @return array + */ + public function getSqlResultSetMappings() + { + return $this->sqlResultSetMappings; + } + /** * Validates & completes the given field mapping. * @@ -1888,7 +1937,7 @@ class ClassMetadataInfo implements ClassMetadata public function addNamedQuery(array $queryMapping) { if (!isset($queryMapping['name'])) { - throw MappingException::nameIsMandatoryQueryMapping($this->name); + throw MappingException::nameIsMandatoryForQueryMapping($this->name); } if (isset($this->namedQueries[$queryMapping['name']])) { @@ -1919,7 +1968,7 @@ class ClassMetadataInfo implements ClassMetadata public function addNamedNativeQuery(array $queryMapping) { if (!isset($queryMapping['name'])) { - throw MappingException::nameIsMandatoryQueryMapping($this->name); + throw MappingException::nameIsMandatoryForQueryMapping($this->name); } if (isset($this->namedNativeQueries[$queryMapping['name']])) { @@ -1941,6 +1990,38 @@ class ClassMetadataInfo implements ClassMetadata $this->namedNativeQueries[$queryMapping['name']] = $queryMapping; } + /** + * INTERNAL: + * Adds a sql result set mapping to this class. + * + * @throws MappingException + * @param array $resultMapping + */ + public function addSqlResultSetMapping(array $resultMapping) + { + if (!isset($resultMapping['name'])) { + throw MappingException::nameIsMandatoryForSqlResultSetMapping($this->name); + } + + if (isset($this->sqlResultSetMappings[$resultMapping['name']])) { + throw MappingException::duplicateResultSetMapping($this->name, $resultMapping['name']); + } + + if (isset($resultMapping['entities'])) { + foreach ($resultMapping['entities'] as $key => $entityResult) { + if (!isset($entityResult['entityClass'])) { + throw MappingException::missingResultSetMappingEntity($this->name, $resultMapping['name']); + } + + if ($entityResult['entityClass'] === '__CLASS__') { + $resultMapping['entities'][$key]['entityClass'] = $this->name; + } + } + } + + $this->sqlResultSetMappings[$resultMapping['name']] = $resultMapping; + } + /** * Adds a one-to-one mapping. * @@ -2154,7 +2235,7 @@ class ClassMetadataInfo implements ClassMetadata /** * Checks whether the class has a named query with the given query name. * - * @param string $fieldName + * @param string $queryName * @return boolean */ public function hasNamedQuery($queryName) @@ -2165,7 +2246,7 @@ class ClassMetadataInfo implements ClassMetadata /** * Checks whether the class has a named native query with the given query name. * - * @param string $fieldName + * @param string $queryName * @return boolean */ public function hasNamedNativeQuery($queryName) @@ -2173,6 +2254,17 @@ class ClassMetadataInfo implements ClassMetadata return isset($this->namedNativeQueries[$queryName]); } + /** + * Checks whether the class has a named native query with the given query name. + * + * @param string $name + * @return boolean + */ + public function hasSqlResultSetMapping($name) + { + return isset($this->sqlResultSetMappings[$name]); + } + /** * Checks whether the class has a mapped association with the given field name. * diff --git a/lib/Doctrine/ORM/Mapping/ColumnResult.php b/lib/Doctrine/ORM/Mapping/ColumnResult.php new file mode 100644 index 000000000..ff5cf272e --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/ColumnResult.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * References name of a column in the SELECT clause of a SQL query. + * Scalar result types can be included in the query result by specifying this annotation in the metadata. + * + * @author Fabio B. Silva + * @since 2.3 + * + * @Annotation + * @Target("ANNOTATION") + */ +final class ColumnResult implements Annotation +{ + + /** + * The name of a column in the SELECT clause of a SQL query + * + * @var string + */ + public $name; + +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index 8d5ffba0e..fffbbcaae 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -93,12 +93,17 @@ class MappingException extends \Doctrine\ORM\ORMException return new self("No query found named '$queryName' on class '$className'."); } + public static function resultMappingNotFound($className, $resultName) + { + return new self("No result set mapping found named '$resultName' on class '$className'."); + } + public static function emptyQueryMapping($entity, $queryName) { return new self('Query named "'.$queryName.'" in "'.$entity.'" could not be empty.'); } - public static function nameIsMandatoryQueryMapping($className) + public static function nameIsMandatoryForQueryMapping($className) { return new self("Query name on entity class '$className' is not defined."); } @@ -108,6 +113,16 @@ class MappingException extends \Doctrine\ORM\ORMException return new self('Query named "'.$queryName.'" in "'.$entity.' requires a result class or result set mapping.'); } + public static function missingResultSetMappingEntity($entity, $resultName) + { + return new self('Result set mapping named "'.$resultName.'" in "'.$entity.' requires a entity class name.'); + } + + public static function nameIsMandatoryForSqlResultSetMapping($className) + { + return new self("Result set mapping name on entity class '$className' is not defined."); + } + public static function oneToManyRequiresMappedBy($fieldName) { return new self("OneToMany mapping on field '$fieldName' requires the 'mappedBy' attribute."); @@ -211,6 +226,11 @@ class MappingException extends \Doctrine\ORM\ORMException return new self('Query named "'.$queryName.'" in "'.$entity.'" was already declared, but it must be declared only once'); } + public static function duplicateResultSetMapping($entity, $resultName) + { + return new self('Result set mapping named "'.$resultName.'" in "'.$entity.'" was already declared, but it must be declared only once'); + } + public static function singleIdNotAllowedOnCompositePrimaryKey($entity) { return new self('Single id is not allowed on composite primary key in entity '.$entity); diff --git a/lib/Doctrine/ORM/Mapping/NamedNativeQueries.php b/lib/Doctrine/ORM/Mapping/NamedNativeQueries.php index f957f4ed2..840d29694 100644 --- a/lib/Doctrine/ORM/Mapping/NamedNativeQueries.php +++ b/lib/Doctrine/ORM/Mapping/NamedNativeQueries.php @@ -37,4 +37,4 @@ final class NamedNativeQueries implements Annotation * @var array<\Doctrine\ORM\Mapping\NamedNativeQuery> */ public $value; -} +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/NamedNativeQuery.php b/lib/Doctrine/ORM/Mapping/NamedNativeQuery.php index f7c604461..022d9801e 100644 --- a/lib/Doctrine/ORM/Mapping/NamedNativeQuery.php +++ b/lib/Doctrine/ORM/Mapping/NamedNativeQuery.php @@ -60,4 +60,4 @@ final class NamedNativeQuery implements Annotation */ public $resultSetMapping; -} +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/SqlResultSetMappings.php b/lib/Doctrine/ORM/Mapping/SqlResultSetMappings.php index 889924bbc..c8c82a916 100644 --- a/lib/Doctrine/ORM/Mapping/SqlResultSetMappings.php +++ b/lib/Doctrine/ORM/Mapping/SqlResultSetMappings.php @@ -37,4 +37,4 @@ final class SqlResultSetMappings implements Annotation * @var array<\Doctrine\ORM\Mapping\SqlResultSetMapping> */ public $value; -} +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index 11f96d712..fd6afe960 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -510,6 +510,29 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals(1, count($cm->getNamedQueries())); } + /** + * @group DDC-1663 + */ + public function testRetrievalOfResultSetMappings() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + + + $this->assertEquals(0, count($cm->getSqlResultSetMappings())); + + $cm->addSqlResultSetMapping(array( + 'name' => 'find-all', + 'entities' => array( + array( + 'entityClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + ), + ), + )); + + $this->assertEquals(1, count($cm->getSqlResultSetMappings())); + } + public function testExistanceOfNamedQuery() { $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); @@ -558,6 +581,85 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $mapping['resultClass']); } + /** + * @group DDC-1663 + */ + public function testRetrieveOfSqlResultSetMapping() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + + $cm->addSqlResultSetMapping(array( + 'name' => 'find-all', + 'entities' => array( + array( + 'entityClass' => '__CLASS__', + 'fields' => array( + array( + 'name' => 'id', + 'column'=> 'id' + ), + array( + 'name' => 'name', + 'column'=> 'name' + ) + ) + ), + array( + 'entityClass' => 'Doctrine\Tests\Models\CMS\CmsEmail', + 'fields' => array( + array( + 'name' => 'id', + 'column'=> 'id' + ), + array( + 'name' => 'email', + 'column'=> 'email' + ) + ) + ) + ), + 'columns' => array( + array( + 'name' => 'scalarColumn' + ) + ) + )); + + $mapping = $cm->getSqlResultSetMapping('find-all'); + + $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $mapping['entities'][0]['entityClass']); + $this->assertEquals(array('name'=>'id','column'=>'id'), $mapping['entities'][0]['fields'][0]); + $this->assertEquals(array('name'=>'name','column'=>'name'), $mapping['entities'][0]['fields'][1]); + + $this->assertEquals('Doctrine\Tests\Models\CMS\CmsEmail', $mapping['entities'][1]['entityClass']); + $this->assertEquals(array('name'=>'id','column'=>'id'), $mapping['entities'][1]['fields'][0]); + $this->assertEquals(array('name'=>'email','column'=>'email'), $mapping['entities'][1]['fields'][1]); + + $this->assertEquals('scalarColumn', $mapping['columns'][0]['name']); + } + + /** + * @group DDC-1663 + */ + public function testExistanceOfSqlResultSetMapping() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + + $cm->addSqlResultSetMapping(array( + 'name' => 'find-all', + 'entities' => array( + array( + 'entityClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + ), + ), + )); + + $this->assertTrue($cm->hasSqlResultSetMapping('find-all')); + $this->assertFalse($cm->hasSqlResultSetMapping('find-by-id')); + } + /** * @group DDC-1663 */ @@ -612,14 +714,15 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals(1, count($cm->getNamedNativeQueries())); } + /** + * @expectedException \Doctrine\ORM\Mapping\MappingException + * @expectedExceptionMessage Query named "userById" in "Doctrine\Tests\Models\CMS\CmsUser" was already declared, but it must be declared only once + */ public function testNamingCollisionNamedQueryShouldThrowException() { $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); - - $this->setExpectedException('Doctrine\ORM\Mapping\MappingException'); - $cm->addNamedQuery(array( 'name' => 'userById', 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' @@ -633,15 +736,15 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase /** * @group DDC-1663 + * + * @expectedException \Doctrine\ORM\Mapping\MappingException + * @expectedExceptionMessage Query named "find-all" in "Doctrine\Tests\Models\CMS\CmsUser" was already declared, but it must be declared only once */ public function testNamingCollisionNamedNativeQueryShouldThrowException() { $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); - - $this->setExpectedException('Doctrine\ORM\Mapping\MappingException'); - $cm->addNamedNativeQuery(array( 'name' => 'find-all', 'query' => 'SELECT * FROM cms_users', @@ -657,6 +760,36 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase )); } + /** + * @group DDC-1663 + * + * @expectedException \Doctrine\ORM\Mapping\MappingException + * @expectedExceptionMessage Result set mapping named "find-all" in "Doctrine\Tests\Models\CMS\CmsUser" was already declared, but it must be declared only once + */ + public function testNamingCollisionSqlResultSetMappingShouldThrowException() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + + $cm->addSqlResultSetMapping(array( + 'name' => 'find-all', + 'entities' => array( + array( + 'entityClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + ), + ), + )); + + $cm->addSqlResultSetMapping(array( + 'name' => 'find-all', + 'entities' => array( + array( + 'entityClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + ), + ), + )); + } + /** * @group DDC-1068 */ @@ -727,6 +860,26 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase )); } + /** + * @group DDC-1663 + * + * @expectedException \Doctrine\ORM\Mapping\MappingException + * @expectedExceptionMessage Result set mapping named "find-all" in "Doctrine\Tests\Models\CMS\CmsUser requires a entity class name. + */ + public function testNameIsMandatoryForEntityNameSqlResultSetMappingException() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + $cm->addSqlResultSetMapping(array( + 'name' => 'find-all', + 'entities' => array( + array( + 'fields' => array() + ) + ), + )); + } + /** * @expectedException \Doctrine\ORM\Mapping\MappingException * @expectedExceptionMessage Discriminator column name on entity class 'Doctrine\Tests\Models\CMS\CmsUser' is not defined.