Skip to content

Commit e43b8d1

Browse files
committed
Support EntityRepository<MyEntity> in phpDoc
1 parent 7c64aa3 commit e43b8d1

File tree

8 files changed

+117
-2
lines changed

8 files changed

+117
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This extension provides following features:
1111

1212
* Provides correct return type for `Doctrine\ORM\EntityManager::find`, `getReference` and `getPartialReference` when `Foo::class` entity class name is provided as the first argument
1313
* Adds missing `matching` method on `Doctrine\Common\Collections\Collection`. This can be turned off by setting `parameters.doctrine.allCollectionsSelectable` to `false`.
14+
* Interpret `EntityRepository<MyEntity>` correctly in phpDocs for further type inference of methods called on the repository.
1415
* Basic DQL validation for parse errors, unknown entity classes and unknown persistent fields.
1516

1617
## Usage

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ services:
3333
objectManagerLoader: %doctrine.objectManagerLoader%
3434
repositoryClass: %doctrine.repositoryClass%
3535

36+
-
37+
class: PHPStan\PhpDoc\Doctrine\EntityRepositoryTypeNodeResolverExtension
38+
tags:
39+
- phpstan.phpDoc.typeNodeResolverExtension
40+
3641
managerRegistryGetRepository:
3742
class: PHPStan\Type\Doctrine\GetRepositoryDynamicReturnTypeExtension
3843
tags:
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDoc\Doctrine;
4+
5+
use PHPStan\PhpDoc\TypeNodeResolver;
6+
use PHPStan\PhpDoc\TypeNodeResolverAwareExtension;
7+
use PHPStan\PhpDoc\TypeNodeResolverExtension;
8+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
9+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
10+
use PHPStan\Type\Doctrine\ObjectRepositoryType;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\TypeWithClassName;
14+
15+
class EntityRepositoryTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension
16+
{
17+
18+
/** @var TypeNodeResolver */
19+
private $typeNodeResolver;
20+
21+
public function setTypeNodeResolver(TypeNodeResolver $typeNodeResolver): void
22+
{
23+
$this->typeNodeResolver = $typeNodeResolver;
24+
}
25+
26+
public function getCacheKey(): string
27+
{
28+
return 'doctrine-v1';
29+
}
30+
31+
public function resolve(TypeNode $typeNode, \PHPStan\Analyser\NameScope $nameScope): ?Type
32+
{
33+
if (!$typeNode instanceof GenericTypeNode) {
34+
return null;
35+
}
36+
37+
if (count($typeNode->genericTypes) !== 1) {
38+
return null;
39+
}
40+
41+
$repositoryType = $this->typeNodeResolver->resolve($typeNode->type, $nameScope);
42+
if (!$repositoryType instanceof TypeWithClassName) {
43+
return null;
44+
}
45+
if (!(new ObjectType('Doctrine\Common\Persistence\ObjectRepository'))->isSuperTypeOf($repositoryType)->yes()) {
46+
return null;
47+
}
48+
49+
$entityType = $this->typeNodeResolver->resolve($typeNode->genericTypes[0], $nameScope);
50+
if (!$entityType instanceof TypeWithClassName) {
51+
return null;
52+
}
53+
54+
return new ObjectRepositoryType($entityType->getClassName(), $repositoryType->getClassName());
55+
}
56+
57+
}

src/Type/Doctrine/ObjectRepositoryType.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type\Doctrine;
44

55
use PHPStan\Type\ObjectType;
6+
use PHPStan\Type\Type;
67
use PHPStan\Type\VerbosityLevel;
78

89
class ObjectRepositoryType extends ObjectType
@@ -27,4 +28,9 @@ public function describe(VerbosityLevel $level): string
2728
return sprintf('%s<%s>', parent::describe($level), $this->entityClass);
2829
}
2930

31+
public static function __set_state(array $properties): Type
32+
{
33+
return new self($properties['entityClass'], $properties['className']);
34+
}
35+
3036
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
[
2+
{
3+
"message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\CustomRepositoryUsage\\MyEntity::nonexistent().",
4+
"line": 35,
5+
"ignorable": true
6+
},
27
{
38
"message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\CustomRepositoryUsage\\MyRepository::nonexistant().",
4-
"line": 30,
9+
"line": 40,
10+
"ignorable": true
11+
},
12+
{
13+
"message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\CustomRepositoryUsage\\MyEntity::nonexistent().",
14+
"line": 47,
515
"ignorable": true
616
}
717
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\CustomRepositoryUsage\\MyEntity::nonexistent().",
4+
"line": 47,
5+
"ignorable": true
6+
}
7+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Cannot call method doSomethingElse() on PHPStan\\DoctrineIntegration\\ORM\\CustomRepositoryUsage\\MyEntity|null.",
4+
"line": 46,
5+
"ignorable": true
6+
},
7+
{
8+
"message": "Cannot call method nonexistent() on PHPStan\\DoctrineIntegration\\ORM\\CustomRepositoryUsage\\MyEntity|null.",
9+
"line": 47,
10+
"ignorable": true
11+
}
12+
]

tests/DoctrineIntegration/ORM/data/customRepositoryUsage.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,38 @@ class Example
1414
*/
1515
private $repository;
1616

17-
public function __construct(EntityManagerInterface $entityManager)
17+
/**
18+
* @var MyRepository<MyEntity>
19+
*/
20+
private $anotherRepository;
21+
22+
public function __construct(
23+
EntityManagerInterface $entityManager,
24+
MyRepository $anotherRepository
25+
)
1826
{
1927
$this->repository = $entityManager->getRepository(MyEntity::class);
28+
$this->anotherRepository = $anotherRepository;
2029
}
2130

2231
public function get(): void
2332
{
2433
$test = $this->repository->get(1);
2534
$test->doSomethingElse();
35+
$test->nonexistent();
2636
}
2737

2838
public function nonexistant(): void
2939
{
3040
$this->repository->nonexistant();
3141
}
42+
43+
public function genericRepository(): void
44+
{
45+
$entity = $this->anotherRepository->find(1);
46+
$entity->doSomethingElse();
47+
$entity->nonexistent();
48+
}
3249
}
3350

3451
/**

0 commit comments

Comments
 (0)