diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index c0dbfafcc..19aacd237 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -56,6 +56,17 @@ + + + + + + + + + + + @@ -63,6 +74,7 @@ + diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index a92ce7355..7bc54c847 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -78,6 +78,17 @@ class EntityRepository implements ObjectRepository ->from($this->_entityName, $alias); } + /** + * Create a new Query instance based on a predefined metadata named query. + * + * @param string $queryName + * @return Query + */ + public function createNamedQuery($queryName) + { + return $this->_em->createQuery($this->_class->getNamedQuery($queryName)); + } + /** * Clears the repository, causing all managed entities to become detached. */ diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index f1b374c5d..a8a8828f6 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -275,6 +275,7 @@ class ClassMetadata extends ClassMetadataInfo { // This metadata is always serialized/cached. $serialized = array( + 'namedQueries', 'associationMappings', 'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName'] 'fieldMappings', diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index b482b1c5e..7db39621d 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -204,6 +204,13 @@ class ClassMetadataInfo implements ClassMetadata */ public $subClasses = array(); + /** + * READ-ONLY: The named queries allowed to be called directly from Repository. + * + * @var array + */ + public $namedQueries = array(); + /** * READ-ONLY: The field names of all fields that are part of the identifier/primary key * of the mapped entity class. @@ -655,6 +662,32 @@ class ClassMetadataInfo implements ClassMetadata $this->fieldNames[$columnName] : $columnName; } + /** + * Gets the named query. + * + * @see ClassMetadataInfo::$namedQueries + * @throws MappingException + * @param string $queryName The query name + * @return string + */ + public function getNamedQuery($queryName) + { + if ( ! isset($this->namedQueries[$queryName])) { + throw MappingException::queryNotFound($this->name, $queryName); + } + return $this->namedQueries[$queryName]; + } + + /** + * Gets all named queries of the class. + * + * @return array + */ + public function getNamedQueries() + { + return $this->namedQueries; + } + /** * Validates & completes the given field mapping. * @@ -1368,8 +1401,7 @@ class ClassMetadataInfo implements ClassMetadata * Adds an association mapping without completing/validating it. * This is mainly used to add inherited association mappings to derived classes. * - * @param AssociationMapping $mapping - * @param string $owningClassName The name of the class that defined this mapping. + * @param array $mapping */ public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/) { @@ -1385,7 +1417,6 @@ class ClassMetadataInfo implements ClassMetadata * This is mainly used to add inherited field mappings to derived classes. * * @param array $mapping - * @todo Rename: addInheritedFieldMapping */ public function addInheritedFieldMapping(array $fieldMapping) { @@ -1394,6 +1425,22 @@ class ClassMetadataInfo implements ClassMetadata $this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName']; } + /** + * INTERNAL: + * Adds a named query to this class. + * + * @throws MappingException + * @param array $queryMapping + */ + public function addNamedQuery(array $queryMapping) + { + if (isset($this->namedQueries[$queryMapping['name']])) { + throw MappingException::duplicateQueryMapping($this->name, $queryMapping['name']); + } + $query = str_replace('__CLASS__', $this->name, $queryMapping['query']); + $this->namedQueries[$queryMapping['name']] = $query; + } + /** * Adds a one-to-one mapping. * @@ -1584,6 +1631,17 @@ class ClassMetadataInfo implements ClassMetadata } } + /** + * Checks whether the class has a named query with the given query name. + * + * @param string $fieldName + * @return boolean + */ + public function hasNamedQuery($queryName) + { + return isset($this->namedQueries[$queryName]); + } + /** * Checks whether the class has a mapped association with the given field name. * diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 01a24f7d4..f976d0aec 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -165,6 +165,18 @@ class AnnotationDriver implements Driver $metadata->setPrimaryTable($primaryTable); } + // Evaluate NamedQueries annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedQueries'])) { + $namedQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedQueries']; + + foreach ($namedQueriesAnnot->value as $namedQuery) { + $metadata->addNamedQuery(array( + 'name' => $namedQuery->name, + 'query' => $namedQuery->query + )); + } + } + // Evaluate InheritanceType annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\InheritanceType'])) { $inheritanceTypeAnnot = $classAnnotations['Doctrine\ORM\Mapping\InheritanceType']; diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index ef566a083..31e712d42 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -127,6 +127,12 @@ final class SequenceGenerator extends Annotation { final class ChangeTrackingPolicy extends Annotation {} final class OrderBy extends Annotation {} +final class NamedQueries extends Annotation {} +final class NamedQuery extends Annotation { + public $name; + public $query; +} + /* Annotations for lifecycle callbacks */ final class HasLifecycleCallbacks extends Annotation {} final class PrePersist extends Annotation {} diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index 3ec712b70..e42d89493 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -69,6 +69,16 @@ class XmlDriver extends AbstractFileDriver $metadata->setPrimaryTable($table); + // Evaluate named queries + if (isset($xmlRoot['named-queries'])) { + foreach ($xmlRoot->{'named-queries'}->{'named-query'} as $namedQueryElement) { + $metadata->addNamedQuery(array( + 'name' => (string)$namedQueryElement['name'], + 'query' => (string)$namedQueryElement['query'] + )); + } + } + /* not implemented specially anyway. use table = schema.table if (isset($xmlRoot['schema'])) { $metadata->table['schema'] = (string)$xmlRoot['schema']; diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 0a66a156e..75bfeec74 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -62,6 +62,21 @@ class YamlDriver extends AbstractFileDriver } $metadata->setPrimaryTable($table); + // Evaluate named queries + if (isset($element['namedQueries'])) { + foreach ($element['namedQueries'] as $name => $queryMapping) { + if (is_string($queryMapping)) { + $queryMapping = array('query' => $queryMapping); + } + + if ( ! isset($queryMapping['name'])) { + $queryMapping['name'] = $name; + } + + $metadata->addNamedQuery($queryMapping); + } + } + /* not implemented specially anyway. use table = schema.table if (isset($element['schema'])) { $metadata->table['schema'] = $element['schema']; diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index f268d0d99..5652cf04b 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -73,6 +73,11 @@ class MappingException extends \Doctrine\ORM\ORMException return new self("No mapping found for field '$fieldName' on class '$className'."); } + public static function queryNotFound($className, $queryName) + { + return new self("No query found named '$queryName' on class '$className'."); + } + public static function oneToManyRequiresMappedBy($fieldName) { return new self("OneToMany mapping on field '$fieldName' requires the 'mappedBy' attribute."); @@ -160,6 +165,10 @@ class MappingException extends \Doctrine\ORM\ORMException return new self('Property "'.$fieldName.'" in "'.$entity.'" was already declared, but it must be declared only once'); } + public static function duplicateQueryMapping($entity, $queryName) { + return new self('Query named "'.$queryName.'" 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/tests/Doctrine/Tests/Models/CMS/CmsUser.php b/tests/Doctrine/Tests/Models/CMS/CmsUser.php index 57741cad1..d9ac982ff 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsUser.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsUser.php @@ -7,6 +7,9 @@ use Doctrine\Common\Collections\ArrayCollection; /** * @Entity * @Table(name="cms_users") + * @NamedQueries({ + * @NamedQuery(name="all", query="SELECT u FROM __CLASS__ u") + * }) */ class CmsUser { diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index ebe2c19f2..a82b09a47 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -288,5 +288,24 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertType('Doctrine\Tests\Models\CMS\CmsAddress', $address); $this->assertEquals($addressId, $address->id); } + + public function testValidNamedQueryRetrieval() + { + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + + $query = $repos->createNamedQuery('all'); + + $this->assertType('Doctrine\ORM\Query', $query); + $this->assertEquals('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u', $query->getDQL()); + } + + public function testInvalidNamedQueryRetrieval() + { + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->setExpectedException('Doctrine\ORM\Mapping\MappingException'); + + $repos->createNamedQuery('invalidNamedQuery'); + } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index f618f6b3a..74889c0ae 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -390,4 +390,60 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); $cm->mapField(array('fieldName' => '')); } + + public function testRetrievalOfNamedQueries() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->assertEquals(0, count($cm->getNamedQueries())); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + + $this->assertEquals(1, count($cm->getNamedQueries())); + } + + public function testExistanceOfNamedQuery() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $cm->addNamedQuery(array( + 'name' => 'all', + 'query' => 'SELECT u FROM __CLASS__ u' + )); + + $this->assertTrue($cm->hasNamedQuery('all')); + $this->assertFalse($cm->hasNamedQuery('userById')); + } + + public function testRetrieveOfNamedQuery() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + + $this->assertEquals('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = ?1', $cm->getNamedQuery('userById')); + } + + public function testNamingCollisionNamedQueryShouldThrowException() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->setExpectedException('Doctrine\ORM\Mapping\MappingException'); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php index 4aadffb30..2abe648ad 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php @@ -10,6 +10,10 @@ $metadata->setChangeTrackingPolicy(ClassMetadataInfo::CHANGETRACKING_DEFERRED_IM $metadata->addLifecycleCallback('doStuffOnPrePersist', 'prePersist'); $metadata->addLifecycleCallback('doOtherStuffOnPrePersistToo', 'prePersist'); $metadata->addLifecycleCallback('doStuffOnPostPersist', 'postPersist'); +$metadata->addNamedQuery(array( + 'name' => 'all', + 'query' => 'SELECT u FROM __CLASS__ u' +)); $metadata->mapField(array( 'id' => true, 'fieldName' => 'id', diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml index 948430c24..c066cbef1 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml @@ -22,6 +22,10 @@ + + + + diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml index b541c8877..a787a93c9 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml @@ -1,6 +1,8 @@ Doctrine\Tests\ORM\Mapping\User: type: entity table: cms_users + namedQueries: + all: SELECT u FROM __CLASS__ u id: id: type: integer