Skip to content

Commit 03bb07f

Browse files
staabmclxmstaab
andauthored
infer PDOStatement generic in $PDO->prepare() with plain queries (#36)
Co-authored-by: Markus Staab <[email protected]>
1 parent 09be3ee commit 03bb07f

File tree

8 files changed

+144
-46
lines changed

8 files changed

+144
-46
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
21
vendor/
32

43
composer.lock
54
.php-cs-fixer.cache
5+
6+
mysqli.php
7+
pdo.php

.phpstan-dba.cache

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,15 @@
214214
array (
215215
1 => NULL,
216216
2 => NULL,
217+
3 => NULL,
217218
),
218219
),
219220
'SELECT email, adaid FROM ada' =>
220221
array (
221222
'error' => NULL,
222223
'result' =>
223224
array (
224-
2 =>
225+
3 =>
225226
PHPStan\Type\Constant\ConstantArrayType::__set_state(array(
226227
'keyType' =>
227228
PHPStan\Type\UnionType::__set_state(array(
@@ -235,6 +236,16 @@
235236
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
236237
'value' => 1,
237238
)),
239+
2 =>
240+
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
241+
'value' => 'adaid',
242+
'isClassString' => false,
243+
)),
244+
3 =>
245+
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
246+
'value' => 'email',
247+
'isClassString' => false,
248+
)),
238249
),
239250
)),
240251
'itemType' =>
@@ -254,10 +265,20 @@
254265
'keyTypes' =>
255266
array (
256267
0 =>
268+
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
269+
'value' => 'email',
270+
'isClassString' => false,
271+
)),
272+
1 =>
257273
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
258274
'value' => 0,
259275
)),
260-
1 =>
276+
2 =>
277+
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
278+
'value' => 'adaid',
279+
'isClassString' => false,
280+
)),
281+
3 =>
261282
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
262283
'value' => 1,
263284
)),
@@ -268,6 +289,14 @@
268289
PHPStan\Type\StringType::__set_state(array(
269290
)),
270291
1 =>
292+
PHPStan\Type\StringType::__set_state(array(
293+
)),
294+
2 =>
295+
PHPStan\Type\IntegerRangeType::__set_state(array(
296+
'min' => 0,
297+
'max' => 4294967295,
298+
)),
299+
3 =>
271300
PHPStan\Type\IntegerRangeType::__set_state(array(
272301
'min' => 0,
273302
'max' => 4294967295,
@@ -279,21 +308,19 @@
279308
),
280309
'allArrays' => NULL,
281310
)),
282-
1 =>
311+
2 =>
283312
PHPStan\Type\Constant\ConstantArrayType::__set_state(array(
284313
'keyType' =>
285314
PHPStan\Type\UnionType::__set_state(array(
286315
'types' =>
287316
array (
288317
0 =>
289-
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
290-
'value' => 'adaid',
291-
'isClassString' => false,
318+
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
319+
'value' => 0,
292320
)),
293321
1 =>
294-
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
295-
'value' => 'email',
296-
'isClassString' => false,
322+
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
323+
'value' => 1,
297324
)),
298325
),
299326
)),
@@ -314,14 +341,12 @@
314341
'keyTypes' =>
315342
array (
316343
0 =>
317-
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
318-
'value' => 'email',
319-
'isClassString' => false,
344+
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
345+
'value' => 0,
320346
)),
321347
1 =>
322-
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
323-
'value' => 'adaid',
324-
'isClassString' => false,
348+
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
349+
'value' => 1,
325350
)),
326351
),
327352
'valueTypes' =>
@@ -335,32 +360,24 @@
335360
'max' => 4294967295,
336361
)),
337362
),
338-
'nextAutoIndex' => 0,
363+
'nextAutoIndex' => 2,
339364
'optionalKeys' =>
340365
array (
341366
),
342367
'allArrays' => NULL,
343368
)),
344-
3 =>
369+
1 =>
345370
PHPStan\Type\Constant\ConstantArrayType::__set_state(array(
346371
'keyType' =>
347372
PHPStan\Type\UnionType::__set_state(array(
348373
'types' =>
349374
array (
350375
0 =>
351-
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
352-
'value' => 0,
353-
)),
354-
1 =>
355-
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
356-
'value' => 1,
357-
)),
358-
2 =>
359376
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
360377
'value' => 'adaid',
361378
'isClassString' => false,
362379
)),
363-
3 =>
380+
1 =>
364381
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
365382
'value' => 'email',
366383
'isClassString' => false,
@@ -389,39 +406,23 @@
389406
'isClassString' => false,
390407
)),
391408
1 =>
392-
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
393-
'value' => 0,
394-
)),
395-
2 =>
396409
PHPStan\Type\Constant\ConstantStringType::__set_state(array(
397410
'value' => 'adaid',
398411
'isClassString' => false,
399412
)),
400-
3 =>
401-
PHPStan\Type\Constant\ConstantIntegerType::__set_state(array(
402-
'value' => 1,
403-
)),
404413
),
405414
'valueTypes' =>
406415
array (
407416
0 =>
408417
PHPStan\Type\StringType::__set_state(array(
409418
)),
410419
1 =>
411-
PHPStan\Type\StringType::__set_state(array(
412-
)),
413-
2 =>
414-
PHPStan\Type\IntegerRangeType::__set_state(array(
415-
'min' => 0,
416-
'max' => 4294967295,
417-
)),
418-
3 =>
419420
PHPStan\Type\IntegerRangeType::__set_state(array(
420421
'min' => 0,
421422
'max' => 4294967295,
422423
)),
423424
),
424-
'nextAutoIndex' => 2,
425+
'nextAutoIndex' => 0,
425426
'optionalKeys' =>
426427
array (
427428
),

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
This extension provides following features:
44

5-
* `PDO->query` knows the array shape of the returned results and therefore can return a generic `PDOStatement`
5+
* the array shape of results can be inferred for `PDOStatement` and `mysqli_result`
6+
* .. when the query string can be resolved at analysis time. This is even possible for queries containing php-variables, as long as their typ is known in most cases.
7+
* builtin we support `mysqli_query`, `mysqli->query`, `PDO->query` and `PDO->prepare`
8+
* `PDO->prepare` knows the array shape of the returned results and therefore can return a generic `PDOStatement`
69
* `mysqli->query` knows the array shape of the returned results and therefore can return a generic `mysqli_result`
710
* `SyntaxErrorInQueryMethodRule` can inspect sql queries and detect syntax errors - `SyntaxErrorInQueryFunctionRule` can do the same for functions
811
* builtin is query syntax error detection for `mysqli_query`, `mysqli->query`, `PDO->query` and `PDO->prepare`

config/extensions.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ services:
44
tags:
55
- phpstan.broker.dynamicMethodReturnTypeExtension
66

7+
-
8+
class: staabm\PHPStanDba\Extensions\PdoPrepareDynamicReturnTypeExtension
9+
tags:
10+
- phpstan.broker.dynamicMethodReturnTypeExtension
11+
712
-
813
class: staabm\PHPStanDba\Extensions\DeployerRunMysqlQueryDynamicReturnTypeExtension
914
tags:
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace staabm\PHPStanDba\Extensions;
6+
7+
use PDO;
8+
use PDOStatement;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Reflection\MethodReflection;
12+
use PHPStan\Type\ArrayType;
13+
use PHPStan\Type\Constant\ConstantBooleanType;
14+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
15+
use PHPStan\Type\Generic\GenericObjectType;
16+
use PHPStan\Type\MixedType;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\TypeCombinator;
19+
use staabm\PHPStanDba\QueryReflection\QueryReflection;
20+
use staabm\PHPStanDba\QueryReflection\QueryReflector;
21+
22+
final class PdoPrepareDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
23+
{
24+
public function getClass(): string
25+
{
26+
return PDO::class;
27+
}
28+
29+
public function isMethodSupported(MethodReflection $methodReflection): bool
30+
{
31+
return 'prepare' === $methodReflection->getName();
32+
}
33+
34+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
35+
{
36+
$args = $methodCall->getArgs();
37+
$mixed = new MixedType(true);
38+
39+
$defaultReturn = TypeCombinator::union(
40+
new GenericObjectType(PDOStatement::class, [new ArrayType($mixed, $mixed)]),
41+
new ConstantBooleanType(false)
42+
);
43+
44+
if (\count($args) < 1) {
45+
return $defaultReturn;
46+
}
47+
48+
$queryReflection = new QueryReflection();
49+
$queryString = $queryReflection->resolveQueryString($args[0]->value, $scope);
50+
if (null === $queryString) {
51+
return $defaultReturn;
52+
}
53+
54+
$reflectionFetchType = QueryReflector::FETCH_TYPE_BOTH;
55+
$resultType = $queryReflection->getResultType($queryString, $reflectionFetchType);
56+
if ($resultType) {
57+
return new GenericObjectType(PDOStatement::class, [$resultType]);
58+
}
59+
60+
return $defaultReturn;
61+
}
62+
}

tests/DbaInferenceTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ public function dataFileAsserts(): iterable
1212
require_once __DIR__.'/data/pdo.php';
1313
yield from $this->gatherAssertTypes(__DIR__.'/data/pdo.php');
1414

15+
// make sure class constants can be resolved
16+
require_once __DIR__.'/data/pdo-prepare.php';
17+
yield from $this->gatherAssertTypes(__DIR__.'/data/pdo-prepare.php');
18+
1519
yield from $this->gatherAssertTypes(__DIR__.'/data/mysqli.php');
1620

1721
// make sure class definitions within the test files are known to reflection

tests/data/pdo-prepare.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace PdoPrepareTest;
4+
5+
use PDO;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
public function prepareSelected(PDO $pdo)
11+
{
12+
$stmt = $pdo->prepare('SELECT email, adaid FROM ada');
13+
$stmt->execute();
14+
assertType('PDOStatement<array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}>', $stmt);
15+
16+
foreach ($stmt as $row) {
17+
assertType('int<0, 4294967295>', $row['adaid']);
18+
assertType('string', $row['email']);
19+
}
20+
}
21+
}

tests/data/pdo.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public function concatedQuerySelected(PDO $pdo, int $int, string $string, float
7373
$stmt = $pdo->query('SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE adaid='.$bool, PDO::FETCH_ASSOC);
7474
assertType('PDOStatement<array{email: string, adaid: int<0, 4294967295>, gesperrt: int<-128, 127>, freigabe1u1: int<-128, 127>}>', $stmt);
7575

76-
// ---- too queries, for which we cannot infer the return type
76+
// ---- queries, for which we cannot infer the return type
7777

7878
$stmt = $pdo->query('SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE '.$string, PDO::FETCH_ASSOC);
7979
assertType('PDOStatement<array>|false', $stmt);

0 commit comments

Comments
 (0)