Skip to content

Commit 29dddd0

Browse files
staabmclxmstaab
andauthored
PDO: Added PDO::FETCH_CLASS support on fetch*(), fetchObject() (#304)
Co-authored-by: Markus Staab <[email protected]>
1 parent 1e28adc commit 29dddd0

File tree

8 files changed

+146
-5
lines changed

8 files changed

+146
-5
lines changed

config/extensions.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ services:
4949
tags:
5050
- phpstan.broker.dynamicMethodReturnTypeExtension
5151

52+
-
53+
class: staabm\PHPStanDba\Extensions\PdoStatementFetchObjectDynamicReturnTypeExtension
54+
tags:
55+
- phpstan.broker.dynamicMethodReturnTypeExtension
56+
5257
-
5358
class: staabm\PHPStanDba\Extensions\PdoStatementColumnCountDynamicReturnTypeExtension
5459
tags:

src/Extensions/PdoStatementFetchDynamicReturnTypeExtension.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
use PHPStan\Php\PhpVersion;
1111
use PHPStan\Reflection\MethodReflection;
1212
use PHPStan\Reflection\ParametersAcceptorSelector;
13+
use PHPStan\Reflection\ReflectionProvider;
1314
use PHPStan\Type\ArrayType;
1415
use PHPStan\Type\Constant\ConstantBooleanType;
1516
use PHPStan\Type\Constant\ConstantIntegerType;
17+
use PHPStan\Type\Constant\ConstantStringType;
1618
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1719
use PHPStan\Type\IntegerType;
1820
use PHPStan\Type\MixedType;
@@ -29,9 +31,15 @@ final class PdoStatementFetchDynamicReturnTypeExtension implements DynamicMethod
2931
*/
3032
private $phpVersion;
3133

32-
public function __construct(PhpVersion $phpVersion)
34+
/**
35+
* @var ReflectionProvider
36+
*/
37+
private $reflectionProvider;
38+
39+
public function __construct(PhpVersion $phpVersion, ReflectionProvider $reflectionProvider)
3340
{
3441
$this->phpVersion = $phpVersion;
42+
$this->reflectionProvider = $reflectionProvider;
3543
}
3644

3745
public function getClass(): string
@@ -91,6 +99,25 @@ private function inferType(MethodReflection $methodReflection, MethodCall $metho
9199
}
92100

93101
$rowType = $pdoStatementReflection->getColumnRowType($statementType, $columnIndex);
102+
} elseif (QueryReflector::FETCH_TYPE_CLASS === $fetchType) {
103+
$className = 'stdClass';
104+
105+
if (\count($args) > 1) {
106+
$classStringType = $scope->getType($args[1]->value);
107+
if ($classStringType instanceof ConstantStringType) {
108+
$className = $classStringType->getValue();
109+
} else {
110+
return null;
111+
}
112+
}
113+
114+
if (!$this->reflectionProvider->hasClass($className)) {
115+
return null;
116+
}
117+
118+
$classString = $this->reflectionProvider->getClass($className)->getName();
119+
120+
$rowType = $pdoStatementReflection->getClassRowType($statementType, $classString);
94121
} else {
95122
$rowType = $pdoStatementReflection->getRowType($statementType, $fetchType);
96123
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace staabm\PHPStanDba\Extensions;
6+
7+
use PDOStatement;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ParametersAcceptorSelector;
12+
use PHPStan\Reflection\ReflectionProvider;
13+
use PHPStan\Type\Constant\ConstantBooleanType;
14+
use PHPStan\Type\Constant\ConstantStringType;
15+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
16+
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\TypeCombinator;
19+
20+
final class PdoStatementFetchObjectDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
21+
{
22+
/**
23+
* @var ReflectionProvider
24+
*/
25+
private $reflectionProvider;
26+
27+
public function __construct(ReflectionProvider $reflectionProvider)
28+
{
29+
$this->reflectionProvider = $reflectionProvider;
30+
}
31+
32+
public function getClass(): string
33+
{
34+
return PDOStatement::class;
35+
}
36+
37+
public function isMethodSupported(MethodReflection $methodReflection): bool
38+
{
39+
return \in_array($methodReflection->getName(), ['fetchObject'], true);
40+
}
41+
42+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
43+
{
44+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
45+
46+
$resultType = $this->inferType($methodReflection, $methodCall, $scope);
47+
if (null !== $resultType) {
48+
$returnType = $resultType;
49+
}
50+
51+
return $returnType;
52+
}
53+
54+
private function inferType(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
55+
{
56+
$args = $methodCall->getArgs();
57+
58+
$className = 'stdClass';
59+
60+
if (\count($args) >= 1) {
61+
$classStringType = $scope->getType($args[0]->value);
62+
if ($classStringType instanceof ConstantStringType) {
63+
$className = $classStringType->getValue();
64+
} else {
65+
return null;
66+
}
67+
}
68+
69+
if (!$this->reflectionProvider->hasClass($className)) {
70+
// XXX should we return NEVER or FALSE on unknown classes?
71+
return null;
72+
}
73+
74+
$classString = $this->reflectionProvider->getClass($className)->getName();
75+
76+
return TypeCombinator::union(new ObjectType($classString), new ConstantBooleanType(false));
77+
}
78+
}

src/PdoReflection/PdoStatementReflection.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Type\Constant\ConstantIntegerType;
1515
use PHPStan\Type\Constant\ConstantStringType;
1616
use PHPStan\Type\Generic\GenericObjectType;
17+
use PHPStan\Type\ObjectType;
1718
use PHPStan\Type\Type;
1819
use PHPStan\Type\TypeCombinator;
1920
use PHPStan\Type\UnionType;
@@ -62,7 +63,9 @@ public function getFetchType(Type $fetchModeType): ?int
6263
return null;
6364
}
6465

65-
if (PDO::FETCH_KEY_PAIR === $fetchModeType->getValue()) {
66+
if (PDO::FETCH_CLASS === $fetchModeType->getValue()) {
67+
return QueryReflector::FETCH_TYPE_CLASS;
68+
} elseif (PDO::FETCH_KEY_PAIR === $fetchModeType->getValue()) {
6669
return QueryReflector::FETCH_TYPE_KEY_VALUE;
6770
} elseif (PDO::FETCH_ASSOC === $fetchModeType->getValue()) {
6871
return QueryReflector::FETCH_TYPE_ASSOC;
@@ -154,6 +157,14 @@ public function getColumnRowType(Type $statementType, int $columnIndex): ?Type
154157
return null;
155158
}
156159

160+
/**
161+
* @param class-string $className
162+
*/
163+
public function getClassRowType(Type $statementType, string $className): ?Type
164+
{
165+
return new ObjectType($className);
166+
}
167+
157168
/**
158169
* @param QueryReflector::FETCH_TYPE* $fetchType
159170
*/

src/QueryReflection/QueryReflector.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ interface QueryReflector
1313
public const FETCH_TYPE_NUMERIC = 4;
1414
public const FETCH_TYPE_BOTH = 5;
1515
public const FETCH_TYPE_KEY_VALUE = 6;
16-
17-
public const FETCH_TYPE_COLUMN = 50;
16+
public const FETCH_TYPE_COLUMN = 7;
17+
public const FETCH_TYPE_CLASS = 8;
1818

1919
public function validateQueryString(string $queryString): ?Error;
2020

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace staabm\PHPStanDba\Tests\Fixture;
4+
5+
class MyRowClass
6+
{
7+
}

tests/default/data/pdo-stmt-fetch.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PDO;
66
use function PHPStan\Testing\assertType;
7+
use staabm\PHPStanDba\Tests\Fixture\MyRowClass;
78

89
class Foo
910
{
@@ -40,6 +41,12 @@ public function fetchAll(PDO $pdo)
4041
$all = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
4142
assertType('array<int, array<string, int<0, 4294967295>>>', $all);
4243

44+
$all = $stmt->fetchAll(PDO::FETCH_CLASS, MyRowClass::class);
45+
assertType('array<int, staabm\PHPStanDba\Tests\Fixture\MyRowClass>', $all);
46+
47+
$all = $stmt->fetchAll(PDO::FETCH_CLASS);
48+
assertType('array<int, stdClass>', $all);
49+
4350
// not yet supported fetch types
4451
$all = $stmt->fetchAll(PDO::FETCH_OBJ);
4552
assertType('array', $all); // XXX since php8 this cannot return false
@@ -78,6 +85,12 @@ public function fetch(PDO $pdo)
7885
$all = $stmt->fetch(PDO::FETCH_KEY_PAIR);
7986
assertType('array<string, int<0, 4294967295>>|false', $all);
8087

88+
$all = $stmt->fetchObject(MyRowClass::class);
89+
assertType('staabm\PHPStanDba\Tests\Fixture\MyRowClass|false', $all);
90+
91+
$all = $stmt->fetchObject();
92+
assertType('stdClass|false', $all);
93+
8194
// not yet supported fetch types
8295
$all = $stmt->fetch(PDO::FETCH_OBJ);
8396
assertType('mixed', $all);

tests/rules/config/.phpstan-dba-mysqli.cache

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)