From 7b842aa9a49a70af923eb2c6953c62d51de07708 Mon Sep 17 00:00:00 2001 From: Michael Moravec Date: Sat, 9 Dec 2017 03:37:53 +0100 Subject: [PATCH] Introduce EntityRepositoryInterface, mandatory ancestor for custom repositories --- docs/en/reference/advanced-configuration.rst | 4 +- docs/en/reference/annotations-reference.rst | 6 +- .../reference/working-with-associations.rst | 2 +- docs/en/reference/working-with-objects.rst | 8 +- docs/en/reference/xml-mapping.rst | 2 +- docs/en/tutorials/getting-started.rst | 6 +- lib/Doctrine/ORM/Configuration.php | 4 +- lib/Doctrine/ORM/EntityManager.php | 2 +- lib/Doctrine/ORM/EntityRepository.php | 140 +++++------------- .../ORM/EntityRepositoryInterface.php | 122 +++++++++++++++ lib/Doctrine/ORM/ORMException.php | 2 +- .../Repository/DefaultRepositoryFactory.php | 13 +- .../ORM/Repository/RepositoryFactory.php | 9 +- .../ORM/Functional/EntityRepositoryTest.php | 2 +- 14 files changed, 187 insertions(+), 135 deletions(-) create mode 100644 lib/Doctrine/ORM/EntityRepositoryInterface.php diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index c83602499cb..caf05e5d8d7 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -411,7 +411,7 @@ implementations. Default Repository (***OPTIONAL***) ----------------------------------- -Specifies the FQCN of a subclass of the EntityRepository. +Specifies the FQCN of a class implementing EntityRepositoryInterface. That will be available for all entities without a custom repository class. .. code-block:: php @@ -421,7 +421,7 @@ That will be available for all entities without a custom repository class. $config->getDefaultRepositoryClassName(); The default value is ``Doctrine\ORM\EntityRepository``. -Any repository class must be a subclass of EntityRepository otherwise you got an ORMException +Any repository class must implement EntityRepositoryInterface otherwise you got an ORMException Setting up the Console ---------------------- diff --git a/docs/en/reference/annotations-reference.rst b/docs/en/reference/annotations-reference.rst index 45ab7e9f1af..d44513c3051 100644 --- a/docs/en/reference/annotations-reference.rst +++ b/docs/en/reference/annotations-reference.rst @@ -377,8 +377,8 @@ the persistence of all classes marked as entities. Optional attributes: -- **repositoryClass**: Specifies the FQCN of a subclass of the - EntityRepository. Use of repositories for entities is encouraged to keep +- **repositoryClass**: Specifies the FQCN of a class implementing the + EntityRepositoryInterface. Use of repositories for entities is encouraged to keep specialized DQL and SQL operations separated from the Model/Domain Layer. - **readOnly**: (>= 2.1) Specifies that this entity is marked as read only and not @@ -799,7 +799,7 @@ The @MappedSuperclass annotation cannot be used in conjunction with Optional attributes: -- **repositoryClass**: (>= 2.2) Specifies the FQCN of a subclass of the EntityRepository. +- **repositoryClass**: (>= 2.2) Specifies the FQCN of a class implementing the EntityRepositoryInterface. That will be inherited for all subclasses of that Mapped Superclass. Example: diff --git a/docs/en/reference/working-with-associations.rst b/docs/en/reference/working-with-associations.rst index 693f543cdaa..87fc5d3245e 100644 --- a/docs/en/reference/working-with-associations.rst +++ b/docs/en/reference/working-with-associations.rst @@ -362,7 +362,7 @@ the details inside the classes can be challenging. This will however always initialize the collection, with all the performance penalties given the size. In some scenarios of large collections it might even be a good idea to completely hide the -read access behind methods on the EntityRepository. +read access behind methods on the EntityRepositoryInterface. There is no single, best way for association management. It greatly depends on the requirements of your concrete domain model as well diff --git a/docs/en/reference/working-with-objects.rst b/docs/en/reference/working-with-objects.rst index b843ac0e4bb..8938dff6e96 100644 --- a/docs/en/reference/working-with-objects.rst +++ b/docs/en/reference/working-with-objects.rst @@ -562,7 +562,7 @@ following: ``EntityManager#getRepository($entityName)`` returns a repository object which provides many ways to retrieve entities of the specified type. By default, the repository instance is of type -``Doctrine\ORM\EntityRepository``. You can also use custom +``Doctrine\ORM\EntityRepositoryInterface``. You can also use custom repository classes as shown later. By Simple Conditions @@ -594,7 +594,7 @@ You can also load by owning side associations through the repository: $number = $em->find('MyProject\Domain\Phonenumber', 1234); $user = $em->getRepository('MyProject\Domain\User')->findOneBy(array('phone' => $number->getId())); -The ``EntityRepository#findBy()`` method additionally accepts orderings, limit and offset as second to fourth parameters: +The ``EntityRepositoryInterface#findBy()`` method additionally accepts orderings, limit and offset as second to fourth parameters: .. code-block:: php @@ -609,7 +609,7 @@ If you pass an array of values Doctrine will convert the query into a WHERE fiel $users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => array(20, 30, 40))); // translates roughly to: SELECT * FROM users WHERE age IN (20, 30, 40) -An EntityRepository also provides a mechanism for more concise +An EntityRepositoryInterface also provides a mechanism for more concise calls through its use of ``__call``. Thus, the following two examples are equivalent: @@ -707,7 +707,7 @@ Custom Repositories ~~~~~~~~~~~~~~~~~~~ By default the EntityManager returns a default implementation of -``Doctrine\ORM\EntityRepository`` when you call +``Doctrine\ORM\EntityRepositoryInterface`` when you call ``EntityManager#getRepository($entityClass)``. You can overwrite this behaviour by specifying the class name of your own Entity Repository in the Annotation, XML or YAML metadata. In large diff --git a/docs/en/reference/xml-mapping.rst b/docs/en/reference/xml-mapping.rst index 469ee8bf616..bac920d0939 100644 --- a/docs/en/reference/xml-mapping.rst +++ b/docs/en/reference/xml-mapping.rst @@ -200,7 +200,7 @@ Optional attributes: - **table** - The Table-Name to be used for this entity. Otherwise the Unqualified Class-Name is used by default. - **repository-class** - The fully qualified class-name of an - alternative ``Doctrine\ORM\EntityRepository`` implementation to be + alternative ``Doctrine\ORM\EntityRepositoryInterface`` implementation to be used with this entity. - **inheritance-type** - The type of inheritance, defaults to none. A more detailed description follows in the diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index 8e2fec8ec08..bbf02a3c129 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -1425,8 +1425,8 @@ example querying for all closed bugs: // do stuff } -Compared to DQL these query methods are falling short of functionality very fast. -Doctrine offers you a convenient way to extend the functionalities of the default ``EntityRepository`` +Compared to DQL these query methods are falling short of functionality very fast. Doctrine offers +you a convenient way to extend the functionalities of the default ``EntityRepositoryInterface`` and put all the specialized DQL query logic on it. For this you have to create a subclass of ``Doctrine\ORM\EntityRepository``, in our case a ``BugRepository`` and group all the previously discussed query functionality in it: @@ -1515,7 +1515,7 @@ we have to adjust the metadata slightly. type: entity repositoryClass: BugRepository -Now we can remove our query logic in all the places and instead use them through the EntityRepository. +Now we can remove our query logic in all the places and instead use them through the repository. As an example here is the code of the first use case "List of Bugs": .. code-block:: php diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index af2c5ef466c..f847b702ec9 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -588,13 +588,13 @@ public function getFilterClassName(string $filterName) : ?string * * @since 2.2 * - * @throws ORMException If not is a \Doctrine\Common\Persistence\ObjectRepository implementation + * @throws ORMException If not is a \Doctrine\ORM\EntityRepositoryInterface implementation */ public function setDefaultRepositoryClassName(string $repositoryClassName) : void { $reflectionClass = new \ReflectionClass($repositoryClassName); - if ( ! $reflectionClass->implementsInterface(ObjectRepository::class)) { + if ( ! $reflectionClass->implementsInterface(EntityRepositoryInterface::class)) { throw ORMException::invalidEntityRepository($repositoryClassName); } diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 141c04d0c99..ec44f763a67 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -717,7 +717,7 @@ public function lock($entity, $lockMode, $lockVersion = null) * * @param string $entityName The name of the entity. * - * @return \Doctrine\Common\Persistence\ObjectRepository|\Doctrine\ORM\EntityRepository The repository class. + * @return EntityRepositoryInterface The repository class. */ public function getRepository($entityName) { diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index 30ddf8ea79d..51b25cb7e96 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -4,10 +4,10 @@ namespace Doctrine\ORM; +use Doctrine\Common\Collections\Collection; use Doctrine\Common\Util\Inflector; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\ResultSetMappingBuilder; -use Doctrine\Common\Persistence\ObjectRepository; -use Doctrine\Common\Collections\Selectable; use Doctrine\Common\Collections\Criteria; /** @@ -23,7 +23,7 @@ * @author Jonathan Wage * @author Roman Borschel */ -class EntityRepository implements ObjectRepository, Selectable +class EntityRepository implements EntityRepositoryInterface { /** * @var string @@ -54,14 +54,9 @@ public function __construct(EntityManagerInterface $em, Mapping\ClassMetadata $c } /** - * Creates a new QueryBuilder instance that is prepopulated for this entity name. - * - * @param string $alias - * @param string $indexBy The index for the from. - * - * @return QueryBuilder + * {@inheritdoc} */ - public function createQueryBuilder($alias, $indexBy = null) + public function createQueryBuilder($alias, $indexBy = null) : QueryBuilder { return $this->em->createQueryBuilder() ->select($alias) @@ -69,15 +64,9 @@ public function createQueryBuilder($alias, $indexBy = null) } /** - * Creates a new result set mapping builder for this entity. - * - * The column naming strategy is "INCREMENT". - * - * @param string $alias - * - * @return ResultSetMappingBuilder + * {@inheritdoc} */ - public function createResultSetMappingBuilder($alias) + public function createResultSetMappingBuilder($alias) : ResultSetMappingBuilder { $rsm = new ResultSetMappingBuilder($this->em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT); $rsm->addRootEntityFromClassMetadata($this->entityName, $alias); @@ -86,13 +75,9 @@ public function createResultSetMappingBuilder($alias) } /** - * Creates a new Query instance based on a predefined metadata named query. - * - * @param string $queryName - * - * @return Query + * {@inheritdoc} */ - public function createNamedQuery($queryName) + public function createNamedQuery($queryName) : Query { $namedQuery = $this->class->getNamedQuery($queryName); $resolvedQuery = str_replace('__CLASS__', $this->class->getClassName(), $namedQuery); @@ -101,13 +86,9 @@ public function createNamedQuery($queryName) } /** - * Creates a native SQL query. - * - * @param string $queryName - * - * @return NativeQuery + * {@inheritdoc} */ - public function createNativeNamedQuery($queryName) + public function createNativeNamedQuery($queryName) : NativeQuery { $queryMapping = $this->class->getNamedNativeQuery($queryName); $rsm = new Query\ResultSetMappingBuilder($this->em); @@ -118,54 +99,33 @@ public function createNativeNamedQuery($queryName) } /** - * Clears the repository, causing all managed entities to become detached. - * - * @return void + * {@inheritdoc} */ - public function clear() + public function clear() : void { $this->em->clear($this->class->getRootClassName()); } /** - * Finds an entity by its primary key / identifier. - * - * @param mixed $id The identifier. - * @param int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants - * or NULL if no specific lock mode should be used - * during the search. - * @param int|null $lockVersion The lock version. - * - * @return object|null The entity instance or NULL if the entity can not be found. + * {@inheritdoc} */ - public function find($id, $lockMode = null, $lockVersion = null) + public function find($id, $lockMode = null, $lockVersion = null) : ?object { return $this->em->find($this->entityName, $id, $lockMode, $lockVersion); } /** - * Finds all entities in the repository. - * - * @return array The entities. + * {@inheritdoc} */ - public function findAll() + public function findAll() : array { return $this->findBy([]); } /** - * Finds entities by a set of criteria. - * - * @param array $criteria - * @param array $orderBy - * @param int|null $limit - * @param int|null $offset - * - * @return array The objects. - * - * @todo guilhermeblanco Change orderBy to use a blank array by default (requires Common\Persistence change). + * {@inheritdoc} */ - public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) : array { $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName); @@ -173,14 +133,9 @@ public function findBy(array $criteria, array $orderBy = null, $limit = null, $o } /** - * Finds a single entity by a set of criteria. - * - * @param array $criteria - * @param array $orderBy - * - * @return object|null The entity instance or NULL if the entity can not be found. + * {@inheritdoc} */ - public function findOneBy(array $criteria, array $orderBy = []) + public function findOneBy(array $criteria, array $orderBy = []) : ?object { $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName); @@ -188,15 +143,9 @@ public function findOneBy(array $criteria, array $orderBy = []) } /** - * Counts entities by a set of criteria. - * - * @todo Add this method to `ObjectRepository` interface in the next major release - * - * @param array $criteria - * - * @return int The cardinality of the objects that match the given criteria. + * {@inheritdoc} */ - public function count(array $criteria) + public function count(array $criteria) : int { return $this->em->getUnitOfWork()->getEntityPersister($this->entityName)->count($criteria); } @@ -204,15 +153,15 @@ public function count(array $criteria) /** * Adds support for magic method calls. * - * @param string $method - * @param array $arguments + * @param string $method + * @param mixed[] $arguments * * @return mixed The returned value from the resolved method. * * @throws ORMException * @throws \BadMethodCallException If the method called is invalid */ - public function __call($method, $arguments) + public function __call(string $method, array $arguments) { if (0 === strpos($method, 'findBy')) { return $this->resolveMagicCall('findBy', substr($method, 6), $arguments); @@ -232,47 +181,30 @@ public function __call($method, $arguments) ); } - /** - * @return string - */ - protected function getEntityName() + protected function getEntityName() : string { return $this->entityName; } - /** - * @return string - */ - public function getClassName() + public function getClassName() : string { return $this->getEntityName(); } - /** - * @return EntityManagerInterface - */ - protected function getEntityManager() + protected function getEntityManager() : EntityManagerInterface { return $this->em; } - /** - * @return Mapping\ClassMetadata - */ - protected function getClassMetadata() + protected function getClassMetadata() : ClassMetadata { return $this->class; } /** - * Select all elements from a selectable that match the expression and - * return a new collection containing these elements. - * - * @param \Doctrine\Common\Collections\Criteria $criteria - * - * @return \Doctrine\Common\Collections\Collection + * {@inheritdoc} */ - public function matching(Criteria $criteria) + public function matching(Criteria $criteria) : Collection { $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName); @@ -282,15 +214,15 @@ public function matching(Criteria $criteria) /** * Resolves a magic method call to the proper existent method at `EntityRepository`. * - * @param string $method The method to call - * @param string $by The property name used as condition - * @param array $arguments The arguments to pass at method call + * @param string $method The method to call + * @param string $by The property name used as condition + * @param mixed[] $arguments The arguments to pass at method call * * @throws ORMException If the method called is invalid or the requested field/association does not exist * * @return mixed */ - private function resolveMagicCall($method, $by, array $arguments) + private function resolveMagicCall(string $method, string $by, array $arguments) { if (! $arguments) { throw ORMException::findByRequiresParameter($method . $by); diff --git a/lib/Doctrine/ORM/EntityRepositoryInterface.php b/lib/Doctrine/ORM/EntityRepositoryInterface.php new file mode 100644 index 00000000000..120d0a19fdb --- /dev/null +++ b/lib/Doctrine/ORM/EntityRepositoryInterface.php @@ -0,0 +1,122 @@ +getClassMetadata($entityName)->getClassName() . spl_object_id($entityManager); @@ -38,12 +39,10 @@ public function getRepository(EntityManagerInterface $entityManager, $entityName /** * Create a new repository instance for an entity class. * - * @param \Doctrine\ORM\EntityManagerInterface $entityManager The EntityManager instance. - * @param string $entityName The name of the entity. - * - * @return \Doctrine\Common\Persistence\ObjectRepository + * @param EntityManagerInterface $entityManager The EntityManager instance. + * @param string $entityName The name of the entity. */ - private function createRepository(EntityManagerInterface $entityManager, $entityName) + private function createRepository(EntityManagerInterface $entityManager, string $entityName) : EntityRepositoryInterface { /* @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */ $metadata = $entityManager->getClassMetadata($entityName); diff --git a/lib/Doctrine/ORM/Repository/RepositoryFactory.php b/lib/Doctrine/ORM/Repository/RepositoryFactory.php index 6977ec97a62..9f02c02c96f 100644 --- a/lib/Doctrine/ORM/Repository/RepositoryFactory.php +++ b/lib/Doctrine/ORM/Repository/RepositoryFactory.php @@ -5,6 +5,7 @@ namespace Doctrine\ORM\Repository; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepositoryInterface; /** * Interface for entity repository factory. @@ -17,10 +18,8 @@ interface RepositoryFactory /** * Gets the repository for an entity class. * - * @param \Doctrine\ORM\EntityManagerInterface $entityManager The EntityManager instance. - * @param string $entityName The name of the entity. - * - * @return \Doctrine\Common\Persistence\ObjectRepository + * @param EntityManagerInterface $entityManager The EntityManager instance. + * @param string $entityName The name of the entity. */ - public function getRepository(EntityManagerInterface $entityManager, $entityName); + public function getRepository(EntityManagerInterface $entityManager, string $entityName) : EntityRepositoryInterface; } diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index e51bf841d8c..bfcd71b6b8e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -646,7 +646,7 @@ public function testDefaultRepositoryClassName() /** * @group DDC-753 * @expectedException Doctrine\ORM\ORMException - * @expectedExceptionMessage Invalid repository class 'Doctrine\Tests\Models\DDC753\DDC753InvalidRepository'. It must be a Doctrine\Common\Persistence\ObjectRepository. + * @expectedExceptionMessage Invalid repository class 'Doctrine\Tests\Models\DDC753\DDC753InvalidRepository'. It must be a Doctrine\ORM\EntityRepositoryInterface. */ public function testSetDefaultRepositoryInvalidClassError() {