diff --git a/extension.neon b/extension.neon index 18bf1904..252da0fd 100644 --- a/extension.neon +++ b/extension.neon @@ -114,6 +114,10 @@ services: class: PHPStan\Type\Doctrine\DoctrineSelectableDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Doctrine\EntityRepositoryMatchingDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension - class: PHPStan\Type\Doctrine\ObjectMetadataResolver arguments: diff --git a/src/Type/Doctrine/EntityRepositoryMatchingDynamicReturnTypeExtension.php b/src/Type/Doctrine/EntityRepositoryMatchingDynamicReturnTypeExtension.php new file mode 100644 index 00000000..0f93e9be --- /dev/null +++ b/src/Type/Doctrine/EntityRepositoryMatchingDynamicReturnTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'matching'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + $callerType = $scope->getType($methodCall->var); + $entityType = $callerType->getTemplateType(EntityRepository::class, 'TEntityClass'); + + return new IntersectionType([ + new GenericObjectType('Doctrine\Common\Collections\Collection', [new IntegerType(), $entityType]), + new GenericObjectType('Doctrine\Common\Collections\Selectable', [new IntegerType(), $entityType]), + ]); + } + +} diff --git a/stubs/EntityManagerInterface.stub b/stubs/EntityManagerInterface.stub index 5fd8024a..c2b3bcd3 100644 --- a/stubs/EntityManagerInterface.stub +++ b/stubs/EntityManagerInterface.stub @@ -2,10 +2,10 @@ namespace Doctrine\ORM; -use Doctrine\Persistence\ObjectManager; -use Doctrine\Persistence\ObjectRepository; +use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Exception\ORMException; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; interface EntityManagerInterface extends ObjectManager { @@ -29,7 +29,7 @@ interface EntityManagerInterface extends ObjectManager /** * @template T of object * @phpstan-param class-string $className - * @phpstan-return ObjectRepository + * @phpstan-return EntityRepository */ public function getRepository($className); diff --git a/stubs/EntityRepository.stub b/stubs/EntityRepository.stub index 77b5b337..8ef22d02 100644 --- a/stubs/EntityRepository.stub +++ b/stubs/EntityRepository.stub @@ -58,15 +58,6 @@ class EntityRepository implements ObjectRepository */ protected function getEntityName(); - /** - * @param \Doctrine\Common\Collections\Criteria $criteria - * - * @return \Doctrine\Common\Collections\Collection - * - * @psalm-return \Doctrine\Common\Collections\Collection - */ - public function matching(Criteria $criteria); - /** * @param __doctrine-literal-string $alias * @param __doctrine-literal-string|null $indexBy diff --git a/tests/DoctrineIntegration/TypeInferenceTest.php b/tests/DoctrineIntegration/TypeInferenceTest.php index 0382118b..634676f3 100644 --- a/tests/DoctrineIntegration/TypeInferenceTest.php +++ b/tests/DoctrineIntegration/TypeInferenceTest.php @@ -12,6 +12,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/getRepository.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/isEmpty.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/Collection.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/repositoryMatching.php'); } /** diff --git a/tests/DoctrineIntegration/data/repositoryMatching.php b/tests/DoctrineIntegration/data/repositoryMatching.php new file mode 100644 index 00000000..8fe4ec11 --- /dev/null +++ b/tests/DoctrineIntegration/data/repositoryMatching.php @@ -0,0 +1,45 @@ +entityManager->getRepository(MyEntity::class); + $criteria = Criteria::create(); + $results = $repository->matching($criteria); + assertType('Doctrine\Common\Collections\Collection&Doctrine\Common\Collections\Selectable', $results); + foreach ($results as $result) { + assertType(MyEntity::class, $result); + } + } + + /** + * @param EntityRepository $repository + */ + public function withTypedRepository(EntityRepository $repository): void + { + $criteria = Criteria::create(); + $results = $repository->matching($criteria); + assertType('Doctrine\Common\Collections\Collection&Doctrine\Common\Collections\Selectable', $results); + foreach ($results as $result) { + assertType(MyEntity::class, $result); + } + } + +} + +class MyEntity +{ + +}