diff --git a/docs/en/reference/working-with-objects.rst b/docs/en/reference/working-with-objects.rst index 8368c9f13..2b52fac41 100644 --- a/docs/en/reference/working-with-objects.rst +++ b/docs/en/reference/working-with-objects.rst @@ -727,6 +727,14 @@ examples are equivalent: // A single user by its nickname (__call magic) $user = $em->getRepository('MyProject\Domain\User')->findOneByNickname('romanb'); +Additionally, you can just count the result of the provided conditions when you don't really need the data: + +.. code-block:: php + + getRepository('MyProject\Domain\User')->count(['nickname' => 'nonexistent']); + By Criteria ~~~~~~~~~~~ diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index ef4734317..05492b133 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -1535,6 +1535,16 @@ As an example here is the code of the first use case "List of Bugs": Using EntityRepositories you can avoid coupling your model with specific query logic. You can also re-use query logic easily throughout your application. +The method ``count()`` takes an array of fields or association keys and the values to match against. +This provides you with a convenient and lightweight way to count a resultset when you don't need to +deal with it: + +.. code-block:: php + + getRepository(Product::class) + ->count(['name' => $productName]); + Conclusion ---------- diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index 08b2ff193..0c2823757 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -19,6 +19,7 @@ namespace Doctrine\ORM; +use Doctrine\Common\Util\Inflector; use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\Common\Persistence\ObjectRepository; use Doctrine\Common\Collections\Selectable; @@ -196,64 +197,48 @@ class EntityRepository implements ObjectRepository, Selectable } /** - * Adds support for magic finders. + * 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. + */ + public function count(array $criteria) + { + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->count($criteria); + } + + /** + * Adds support for magic method calls. * * @param string $method * @param array $arguments * - * @return array|object The found entity/entities. + * @return mixed The returned value from the resolved method. * * @throws ORMException - * @throws \BadMethodCallException If the method called is an invalid find* method - * or no find* method at all and therefore an invalid - * method call. + * @throws \BadMethodCallException If the method called is invalid */ public function __call($method, $arguments) { - switch (true) { - case (0 === strpos($method, 'findBy')): - $by = substr($method, 6); - $method = 'findBy'; - break; - - case (0 === strpos($method, 'findOneBy')): - $by = substr($method, 9); - $method = 'findOneBy'; - break; - - default: - throw new \BadMethodCallException( - "Undefined method '$method'. The method name must start with ". - "either findBy or findOneBy!" - ); + if (0 === strpos($method, 'findBy')) { + return $this->resolveMagicCall('findBy', substr($method, 6), $arguments); } - if (empty($arguments)) { - throw ORMException::findByRequiresParameter($method . $by); + if (0 === strpos($method, 'findOneBy')) { + return $this->resolveMagicCall('findOneBy', substr($method, 9), $arguments); } - $fieldName = lcfirst(\Doctrine\Common\Util\Inflector::classify($by)); - - if ($this->_class->hasField($fieldName) || $this->_class->hasAssociation($fieldName)) { - switch (count($arguments)) { - case 1: - return $this->$method(array($fieldName => $arguments[0])); - - case 2: - return $this->$method(array($fieldName => $arguments[0]), $arguments[1]); - - case 3: - return $this->$method(array($fieldName => $arguments[0]), $arguments[1], $arguments[2]); - - case 4: - return $this->$method(array($fieldName => $arguments[0]), $arguments[1], $arguments[2], $arguments[3]); - - default: - // Do nothing - } + if (0 === strpos($method, 'countBy')) { + return $this->resolveMagicCall('count', substr($method, 7), $arguments); } - throw ORMException::invalidFindByCall($this->_entityName, $fieldName, $method.$by); + throw new \BadMethodCallException( + "Undefined method '$method'. The method name must start with ". + "either findBy or findOneBy!" + ); } /** @@ -302,4 +287,30 @@ class EntityRepository implements ObjectRepository, Selectable return new LazyCriteriaCollection($persister, $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 + * + * @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) + { + if (! $arguments) { + throw ORMException::findByRequiresParameter($method . $by); + } + + $fieldName = lcfirst(Inflector::classify($by)); + + if (! ($this->_class->hasField($fieldName) || $this->_class->hasAssociation($fieldName))) { + throw ORMException::invalidMagicCall($this->_entityName, $fieldName, $method . $by); + } + + return $this->$method([$fieldName => $arguments[0]], ...array_slice($arguments, 1)); + } } diff --git a/lib/Doctrine/ORM/ORMException.php b/lib/Doctrine/ORM/ORMException.php index 6d8a6d631..919789d92 100644 --- a/lib/Doctrine/ORM/ORMException.php +++ b/lib/Doctrine/ORM/ORMException.php @@ -187,6 +187,21 @@ class ORMException extends Exception ); } + /** + * @param string $entityName + * @param string $fieldName + * @param string $method + * + * @return ORMException + */ + public static function invalidMagicCall($entityName, $fieldName, $method) + { + return new self( + "Entity '".$entityName."' has no field '".$fieldName."'. ". + "You can therefore not call '".$method."' on the entities' repository" + ); + } + /** * @param string $entityName * @param string $associationFieldName diff --git a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php index 3b2faf878..c62a10a12 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php @@ -822,7 +822,7 @@ class BasicEntityPersister implements EntityPersister ? $this->expandCriteriaParameters($criteria) : $this->expandParameters($criteria); - return $this->conn->executeQuery($sql, $params, $types)->fetchColumn(); + return (int) $this->conn->executeQuery($sql, $params, $types)->fetchColumn(); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index ffca89ba0..e0b96387a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -272,6 +272,30 @@ class EntityRepositoryTest extends OrmFunctionalTestCase $this->assertEquals(4, count($users)); } + public function testCount() + { + $this->loadFixture(); + $repos = $this->_em->getRepository(CmsUser::class); + + $userCount = $repos->count(array()); + $this->assertSame(4, $userCount); + + $userCount = $repos->count(array('status' => 'dev')); + $this->assertSame(2, $userCount); + + $userCount = $repos->count(array('status' => 'nonexistent')); + $this->assertSame(0, $userCount); + } + + public function testCountBy() + { + $this->loadFixture(); + $repos = $this->_em->getRepository(CmsUser::class); + + $userCount = $repos->countByStatus('dev'); + $this->assertSame(2, $userCount); + } + /** * @expectedException \Doctrine\ORM\ORMException */