Skip to content

Commit d4df070

Browse files
staabmclxmstaab
andauthored
Support PDOStatement->fetchAll() (#40)
Co-authored-by: Markus Staab <[email protected]>
1 parent 251be93 commit d4df070

File tree

5 files changed

+129
-1
lines changed

5 files changed

+129
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This extension provides following features:
44

55
* the array shape of results can be inferred for `PDOStatement` and `mysqli_result`
66
* .. 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`
7+
* builtin we support `mysqli_query`, `mysqli->query`, `PDOStatement->fetchAll`, `PDO->query` and `PDO->prepare`
88
* `PDO->prepare` knows the array shape of the returned results and therefore can return a generic `PDOStatement`
99
* `mysqli->query` knows the array shape of the returned results and therefore can return a generic `mysqli_result`
1010
* `SyntaxErrorInQueryMethodRule` can inspect sql queries and detect syntax errors - `SyntaxErrorInQueryFunctionRule` can do the same for functions

config/extensions.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ services:
99
tags:
1010
- phpstan.broker.dynamicMethodReturnTypeExtension
1111

12+
-
13+
class: staabm\PHPStanDba\Extensions\PdoStatementFetchDynamicReturnTypeExtension
14+
tags:
15+
- phpstan.broker.dynamicMethodReturnTypeExtension
16+
1217
-
1318
class: staabm\PHPStanDba\Extensions\DeployerRunMysqlQueryDynamicReturnTypeExtension
1419
tags:
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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\Reflection\ParametersAcceptorSelector;
13+
use PHPStan\Type\ArrayType;
14+
use PHPStan\Type\Constant\ConstantArrayType;
15+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
16+
use PHPStan\Type\Constant\ConstantIntegerType;
17+
use PHPStan\Type\Constant\ConstantStringType;
18+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
19+
use PHPStan\Type\Generic\GenericObjectType;
20+
use PHPStan\Type\IntegerType;
21+
use PHPStan\Type\Type;
22+
23+
final class PdoStatementFetchDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
24+
{
25+
public function getClass(): string
26+
{
27+
return PDOStatement::class;
28+
}
29+
30+
public function isMethodSupported(MethodReflection $methodReflection): bool
31+
{
32+
return 'fetchAll' === $methodReflection->getName();
33+
}
34+
35+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
36+
{
37+
$args = $methodCall->getArgs();
38+
$defaultReturn = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
39+
40+
$statementType = $scope->getType($methodCall->var);
41+
42+
if ($statementType instanceof GenericObjectType) {
43+
$genericTypes = $statementType->getTypes();
44+
45+
if (1 !== \count($genericTypes)) {
46+
return $defaultReturn;
47+
}
48+
49+
$fetchType = PDO::FETCH_BOTH;
50+
if (\count($args) > 0) {
51+
$fetchModeType = $scope->getType($args[0]->value);
52+
if (!$fetchModeType instanceof ConstantIntegerType) {
53+
return $defaultReturn;
54+
}
55+
$fetchType = $fetchModeType->getValue();
56+
57+
if (!\in_array($fetchType, [PDO::FETCH_ASSOC, PDO::FETCH_NUM, PDO::FETCH_BOTH])) {
58+
return $defaultReturn;
59+
}
60+
}
61+
62+
$resultType = $genericTypes[0];
63+
64+
if ((PDO::FETCH_NUM === $fetchType || PDO::FETCH_ASSOC === $fetchType) && $resultType instanceof ConstantArrayType) {
65+
$builder = ConstantArrayTypeBuilder::createEmpty();
66+
67+
$keyTypes = $resultType->getKeyTypes();
68+
$valueTypes = $resultType->getValueTypes();
69+
70+
foreach ($keyTypes as $i => $keyType) {
71+
if (PDO::FETCH_NUM === $fetchType && $keyType instanceof ConstantIntegerType) {
72+
$builder->setOffsetValueType($keyType, $valueTypes[$i]);
73+
} elseif (PDO::FETCH_ASSOC === $fetchType && $keyType instanceof ConstantStringType) {
74+
$builder->setOffsetValueType($keyType, $valueTypes[$i]);
75+
}
76+
}
77+
78+
return new ArrayType(new IntegerType(), $builder->getArray());
79+
}
80+
81+
return new ArrayType(new IntegerType(), $resultType);
82+
}
83+
84+
return $defaultReturn;
85+
}
86+
}

tests/DbaInferenceTest.php

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

23+
// make sure class constants can be resolved
24+
require_once __DIR__.'/data/pdo-stmt-fetch.php';
25+
yield from $this->gatherAssertTypes(__DIR__.'/data/pdo-stmt-fetch.php');
26+
2327
// make sure class constants can be resolved
2428
require_once __DIR__.'/data/pdo-fetch-types.php';
2529
yield from $this->gatherAssertTypes(__DIR__.'/data/pdo-fetch-types.php');

tests/data/pdo-stmt-fetch.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace PdoStmtFetchTest;
4+
5+
use PDO;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
public function fetchAll(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+
// default fetch-mode is BOTH
17+
$all = $stmt->fetchAll();
18+
assertType('array<int, array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}>', $all);
19+
20+
$all = $stmt->fetchAll(PDO::FETCH_BOTH);
21+
assertType('array<int, array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}>', $all);
22+
23+
$all = $stmt->fetchAll(PDO::FETCH_NUM);
24+
assertType('array<int, array{string, int<0, 4294967295>}>', $all);
25+
26+
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);
27+
assertType('array<int, array{email: string, adaid: int<0, 4294967295>}>', $all);
28+
29+
// not yet supported fetch types
30+
$all = $stmt->fetchAll(PDO::FETCH_OBJ);
31+
assertType('array|false', $all); // XXX since php8 this cannot return false
32+
}
33+
}

0 commit comments

Comments
 (0)