diff --git a/extension.neon b/extension.neon index 18bf1904..8e2bfcca 100644 --- a/extension.neon +++ b/extension.neon @@ -32,6 +32,7 @@ parameters: - stubs/Persistence/ObjectManagerDecorator.stub - stubs/Persistence/ObjectRepository.stub - stubs/RepositoryFactory.stub + - stubs/Collections/AbstractLazyCollection.stub - stubs/Collections/ArrayCollection.stub - stubs/Collections/Selectable.stub - stubs/ORM/AbstractQuery.stub diff --git a/stubs/Collections/AbstractLazyCollection.stub b/stubs/Collections/AbstractLazyCollection.stub new file mode 100644 index 00000000..bb00b3b0 --- /dev/null +++ b/stubs/Collections/AbstractLazyCollection.stub @@ -0,0 +1,13 @@ + + * @template-implements Selectable + */ +abstract class AbstractLazyCollection implements Collection, Selectable +{ +} 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..4b97676d 100644 --- a/stubs/EntityRepository.stub +++ b/stubs/EntityRepository.stub @@ -2,7 +2,9 @@ namespace Doctrine\ORM; +use Doctrine\Common\Collections\AbstractLazyCollection; use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\Selectable; use Doctrine\Persistence\ObjectRepository; /** @@ -59,11 +61,9 @@ class EntityRepository implements ObjectRepository protected function getEntityName(); /** - * @param \Doctrine\Common\Collections\Criteria $criteria + * @param Criteria $criteria * - * @return \Doctrine\Common\Collections\Collection - * - * @psalm-return \Doctrine\Common\Collections\Collection + * @phpstan-return AbstractLazyCollection&Selectable */ public function matching(Criteria $criteria); diff --git a/tests/DoctrineIntegration/TypeInferenceTest.php b/tests/DoctrineIntegration/TypeInferenceTest.php index 0382118b..fb8efda4 100644 --- a/tests/DoctrineIntegration/TypeInferenceTest.php +++ b/tests/DoctrineIntegration/TypeInferenceTest.php @@ -2,7 +2,10 @@ namespace PHPStan\DoctrineIntegration; +use Doctrine\Common\Collections\AbstractLazyCollection; +use Doctrine\Common\Collections\Selectable; use PHPStan\Testing\TypeInferenceTestCase; +use function is_a; class TypeInferenceTest extends TypeInferenceTestCase { @@ -12,6 +15,12 @@ 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'); + + if (is_a(AbstractLazyCollection::class, Selectable::class, true)) { // @phpstan-ignore function.alreadyNarrowedType + yield from $this->gatherAssertTypes(__DIR__ . '/data/repositoryMatching-collection26.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/repositoryMatching-collection25.php'); + } } /** diff --git a/tests/DoctrineIntegration/data/repositoryMatching-collection25.php b/tests/DoctrineIntegration/data/repositoryMatching-collection25.php new file mode 100644 index 00000000..9aabef83 --- /dev/null +++ b/tests/DoctrineIntegration/data/repositoryMatching-collection25.php @@ -0,0 +1,45 @@ +entityManager->getRepository(MyEntity::class); + $criteria = Criteria::create(); + $results = $repository->matching($criteria); + assertType('Doctrine\Common\Collections\AbstractLazyCollection&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\AbstractLazyCollection&Doctrine\Common\Collections\Selectable', $results); + foreach ($results as $result) { + assertType(MyEntity::class, $result); + } + } + +} + +class MyEntity +{ + +} diff --git a/tests/DoctrineIntegration/data/repositoryMatching-collection26.php b/tests/DoctrineIntegration/data/repositoryMatching-collection26.php new file mode 100644 index 00000000..f76dfa30 --- /dev/null +++ b/tests/DoctrineIntegration/data/repositoryMatching-collection26.php @@ -0,0 +1,45 @@ +entityManager->getRepository(MyEntity::class); + $criteria = Criteria::create(); + $results = $repository->matching($criteria); + assertType('Doctrine\Common\Collections\AbstractLazyCollection', $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\AbstractLazyCollection', $results); + foreach ($results as $result) { + assertType(MyEntity::class, $result); + } + } + +} + +class MyEntity +{ + +}