Skip to content

Commit 3313ae0

Browse files
authored
QueryBuilder: allow dynamic args in methods not affecting result type
1 parent b0e0c32 commit 3313ae0

File tree

4 files changed

+204
-0
lines changed

4 files changed

+204
-0
lines changed

src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@
3232
class QueryBuilderGetQueryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
3333
{
3434

35+
/**
36+
* Those are critical methods where we need to understand arguments passed to them, the rest is allowed to be more dynamic
37+
* - this list reflects what is implemented in QueryResultTypeWalker
38+
*/
39+
private const METHODS_AFFECTING_RESULT_TYPE = [
40+
'add',
41+
'select',
42+
'addselect',
43+
'from',
44+
'join',
45+
'innerjoin',
46+
'leftjoin',
47+
'indexby',
48+
];
49+
3550
/** @var ObjectMetadataResolver */
3651
private $objectMetadataResolver;
3752

@@ -139,6 +154,9 @@ public function getTypeFromMethodCall(
139154
try {
140155
$args = $this->argumentsProcessor->processArgs($scope, $methodName, $calledMethodCall->getArgs());
141156
} catch (DynamicQueryBuilderArgumentException $e) {
157+
if (!in_array($lowerMethodName, self::METHODS_AFFECTING_RESULT_TYPE, true)) {
158+
continue;
159+
}
142160
return $defaultReturnType;
143161
}
144162

tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,19 @@ public function testBranchingPerformance(): void
134134
]);
135135
}
136136

137+
public function testDynamicWhere(): void
138+
{
139+
$this->analyse([__DIR__ . '/data/query-builder-dynamic.php'], [
140+
['Could not analyse QueryBuilder with dynamic arguments.', 40],
141+
['Could not analyse QueryBuilder with dynamic arguments.', 45],
142+
['Could not analyse QueryBuilder with dynamic arguments.', 51],
143+
['Could not analyse QueryBuilder with dynamic arguments.', 56],
144+
['Could not analyse QueryBuilder with dynamic arguments.', 61],
145+
['Could not analyse QueryBuilder with dynamic arguments.', 66],
146+
['Could not analyse QueryBuilder with dynamic arguments.', 71],
147+
]);
148+
}
149+
137150
public static function getAdditionalConfigFiles(): array
138151
{
139152
return [
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use Doctrine\Common\Collections\Criteria;
6+
use Doctrine\ORM\EntityManagerInterface;
7+
use Doctrine\ORM\Query\Expr\Andx;
8+
use Doctrine\ORM\Query\Expr\From;
9+
10+
class DynamicCalls
11+
{
12+
public function testDynamicMethodCall(
13+
EntityManagerInterface $em,
14+
Andx $and,
15+
Criteria $criteria,
16+
string $string
17+
): void
18+
{
19+
$em->createQueryBuilder()
20+
->select('m')
21+
->from(MyEntity::class, 'm')
22+
->andWhere($and)
23+
->setParameter($string, $string)
24+
->setParameters([$string])
25+
->orWhere($string)
26+
->addOrderBy($string)
27+
->addGroupBy($string)
28+
->addCriteria($criteria)
29+
->getQuery();
30+
31+
$em->createQueryBuilder()
32+
->select('m')
33+
->add('from', new From(MyEntity::class, 'm', null), true)
34+
->where($string)
35+
->orderBy($string)
36+
->groupBy($string)
37+
->getQuery();
38+
39+
// all below are disallowed dynamic
40+
$em->createQueryBuilder()
41+
->select('m')
42+
->from($string, 'm')
43+
->getQuery();
44+
45+
$em->createQueryBuilder()
46+
->select('m')
47+
->from(MyEntity::class, 'm')
48+
->indexBy($string, $string)
49+
->getQuery();
50+
51+
$em->createQueryBuilder()
52+
->select('m')
53+
->from(MyEntity::class, 'm', $string)
54+
->getQuery();
55+
56+
$em->createQueryBuilder()
57+
->select([$string])
58+
->from(MyEntity::class, 'm')
59+
->getQuery();
60+
61+
$em->createQueryBuilder()
62+
->select(['m'])
63+
->from(MyEntity::class, $string)
64+
->getQuery();
65+
66+
$em->createQueryBuilder()
67+
->addSelect($string)
68+
->from(MyEntity::class, 'm')
69+
->getQuery();
70+
71+
$em->createQueryBuilder()
72+
->addSelect('m')
73+
->from(MyEntity::class, 'm')
74+
->join($string, $string)
75+
->getQuery();
76+
}
77+
78+
}

tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
namespace QueryResult\CreateQuery;
44

5+
use Doctrine\Common\Collections\Criteria;
56
use Doctrine\ORM\AbstractQuery;
67
use Doctrine\ORM\EntityManagerInterface;
8+
use Doctrine\ORM\Query\Expr\Andx;
9+
use Doctrine\ORM\Query\Expr\From;
710
use Doctrine\ORM\QueryBuilder;
811
use QueryResult\Entities\Many;
912
use function PHPStan\Testing\assertType;
@@ -135,7 +138,99 @@ public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterfa
135138
->getQuery();
136139

137140
assertType('Doctrine\ORM\Query<void, void>', $query);
141+
}
142+
143+
144+
public function testDynamicMethodCall(
145+
EntityManagerInterface $em,
146+
Andx $and,
147+
Criteria $criteria,
148+
string $string
149+
): void
150+
{
151+
$result = $em->createQueryBuilder()
152+
->select('m')
153+
->from(Many::class, 'm')
154+
->andWhere($and)
155+
->setParameter($string, $string)
156+
->setParameters([$string])
157+
->orWhere($string)
158+
->addOrderBy($string)
159+
->addGroupBy($string)
160+
->addCriteria($criteria)
161+
->getQuery()
162+
->getResult();
163+
164+
assertType('list<QueryResult\Entities\Many>', $result);
165+
166+
$result = $em->createQueryBuilder()
167+
->select(['m.stringNullColumn'])
168+
->add('from', new From(Many::class, 'm', null), true)
169+
->where($string)
170+
->orderBy($string)
171+
->groupBy($string)
172+
->getQuery()
173+
->getResult();
174+
175+
assertType('list<array{stringNullColumn: string|null}>', $result);
176+
177+
$result = $em->createQueryBuilder()
178+
->select(['m.intColumn', 'm.stringNullColumn'])
179+
->from($string, 'm')
180+
->getQuery()
181+
->getResult();
182+
183+
assertType('mixed', $result);
184+
185+
$result = $em->createQueryBuilder()
186+
->select(['m.intColumn', 'm.stringNullColumn'])
187+
->from(Many::class, 'm')
188+
->indexBy($string, $string)
189+
->getQuery()
190+
->getResult();
191+
192+
assertType('mixed', $result);
193+
194+
$result = $em->createQueryBuilder()
195+
->select('m')
196+
->from(Many::class, 'm', $string)
197+
->getQuery()
198+
->getResult();
199+
200+
assertType('mixed', $result);
201+
202+
$result = $em->createQueryBuilder()
203+
->select([$string, 'm.stringNullColumn'])
204+
->from(Many::class, 'm')
205+
->getQuery()
206+
->getResult();
207+
208+
assertType('mixed', $result);
209+
210+
$result = $em->createQueryBuilder()
211+
->select(['m.stringNullColumn'])
212+
->from(Many::class, $string)
213+
->getQuery()
214+
->getResult();
215+
216+
assertType('mixed', $result);
217+
218+
$result = $em->createQueryBuilder()
219+
->addSelect($string)
220+
->from(Many::class, 'm')
221+
->getQuery()
222+
->getResult();
223+
224+
assertType('mixed', $result);
225+
226+
$result = $em->createQueryBuilder()
227+
->addSelect('m')
228+
->from(Many::class, 'm')
229+
->join($string, $string)
230+
->getQuery()
231+
->getResult();
138232

233+
assertType('mixed', $result);
139234
}
140235

141236
public function testQueryTypeIsInferredOnAcrossMethods(EntityManagerInterface $em): void

0 commit comments

Comments
 (0)