Skip to content

Commit c57077b

Browse files
authored
Implement dynamic return type extension for EntityRepository::createQueryBuilder() (#140)
1 parent 207a6ae commit c57077b

File tree

6 files changed

+97
-0
lines changed

6 files changed

+97
-0
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ services:
6464
fasterVersion: %doctrine.queryBuilderFastAlgorithm%
6565
tags:
6666
- phpstan.broker.dynamicMethodReturnTypeExtension
67+
-
68+
class: PHPStan\Type\Doctrine\QueryBuilder\EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension
69+
tags:
70+
- phpstan.broker.dynamicMethodReturnTypeExtension
6771
-
6872
class: PHPStan\Type\Doctrine\DoctrineSelectableDynamicReturnTypeExtension
6973
tags:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\QueryBuilder;
4+
5+
use PhpParser\Node\Arg;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\Node\Identifier;
8+
use PhpParser\Node\Scalar\String_;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
12+
use PHPStan\Type\Generic\GenericClassStringType;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\TypeWithClassName;
15+
16+
class EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
17+
{
18+
19+
public function getClass(): string
20+
{
21+
return 'Doctrine\ORM\EntityRepository';
22+
}
23+
24+
public function isMethodSupported(MethodReflection $methodReflection): bool
25+
{
26+
return $methodReflection->getName() === 'createQueryBuilder';
27+
}
28+
29+
public function getTypeFromMethodCall(
30+
MethodReflection $methodReflection,
31+
MethodCall $methodCall,
32+
Scope $scope
33+
): Type
34+
{
35+
$entityNameExpr = new MethodCall($methodCall->var, new Identifier('getEntityName'));
36+
37+
$entityNameExprType = $scope->getType($entityNameExpr);
38+
if ($entityNameExprType instanceof GenericClassStringType && $entityNameExprType->getGenericType() instanceof TypeWithClassName) {
39+
$entityNameExpr = new String_($entityNameExprType->getGenericType()->getClassName());
40+
}
41+
42+
$fromArgs = $methodCall->args;
43+
array_unshift($fromArgs, new Arg($entityNameExpr));
44+
45+
$callStack = new MethodCall($methodCall->var, new Identifier('getEntityManager'));
46+
$callStack = new MethodCall($callStack, new Identifier('createQueryBuilder'));
47+
$callStack = new MethodCall($callStack, new Identifier('select'), [$methodCall->args[0]]);
48+
$callStack = new MethodCall($callStack, new Identifier('from'), $fromArgs);
49+
50+
return $scope->getType($callStack);
51+
}
52+
53+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Could not analyse QueryBuilder with dynamic arguments.",
4+
"line": 54,
5+
"ignorable": true
6+
}
7+
]

tests/DoctrineIntegration/ORM/data/queryBuilder-4.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,15 @@
88
"message": "Strict comparison using === between 'SELECT e FROM…' and 'aaa' will always evaluate to false.",
99
"line": 30,
1010
"ignorable": true
11+
},
12+
{
13+
"message": "Strict comparison using === between 'SELECT e FROM…' and 'bbb' will always evaluate to false.",
14+
"line": 41,
15+
"ignorable": true
16+
},
17+
{
18+
"message": "Strict comparison using === between 'SELECT e FROM…' and 'bbb' will always evaluate to false.",
19+
"line": 42,
20+
"ignorable": true
1121
}
1222
]

tests/DoctrineIntegration/ORM/data/queryBuilder.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,26 @@ public function doFoo(): Query
3232
return $query;
3333
}
3434

35+
public function doBar(): Query
36+
{
37+
$entityRepository = $this->entityManager->getRepository(MyEntity::class);
38+
$queryBuilder = $entityRepository->createQueryBuilder('e');
39+
$query = $queryBuilder->getQuery();
40+
41+
$query->getDQL() === 'bbb';
42+
$queryBuilder->getDQL() === 'bbb';
43+
44+
return $query;
45+
}
46+
47+
/**
48+
* @phpstan-param class-string $entityClass
49+
*/
50+
public function dynamicQueryBuilder(string $entityClass): Query
51+
{
52+
$entityRepository = $this->entityManager->getRepository($entityClass);
53+
$queryBuilder = $entityRepository->createQueryBuilder('e');
54+
return $queryBuilder->getQuery();
55+
}
56+
3557
}

tests/DoctrineIntegration/ORM/phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ includes:
66
parameters:
77
doctrine:
88
objectManagerLoader: entity-manager.php
9+
reportDynamicQueryBuilders: true

0 commit comments

Comments
 (0)