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