Skip to content

Commit 6fb6b07

Browse files
staabmclxmstaab
andauthored
PDO: support PDOStatement::setFetchMode() (#258)
Co-authored-by: Markus Staab <[email protected]>
1 parent 91ceba1 commit 6fb6b07

File tree

5 files changed

+157
-1
lines changed

5 files changed

+157
-1
lines changed

config/extensions.neon

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

5252
-
53+
class: staabm\PHPStanDba\Extensions\PdoStatementSetFetchModeTypeSpecifyingExtension
54+
tags:
55+
- phpstan.typeSpecifier.methodTypeSpecifyingExtension
56+
57+
-
5358
class: staabm\PHPStanDba\Extensions\PdoStatementFetchObjectDynamicReturnTypeExtension
5459
tags:
5560
- phpstan.broker.dynamicMethodReturnTypeExtension
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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\Analyser\SpecifiedTypes;
11+
use PHPStan\Analyser\TypeSpecifier;
12+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
13+
use PHPStan\Analyser\TypeSpecifierContext;
14+
use PHPStan\Reflection\MethodReflection;
15+
use PHPStan\Type\Generic\GenericObjectType;
16+
use PHPStan\Type\MethodTypeSpecifyingExtension;
17+
use staabm\PHPStanDba\PdoReflection\PdoStatementReflection;
18+
19+
final class PdoStatementSetFetchModeTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
20+
{
21+
private TypeSpecifier $typeSpecifier;
22+
23+
public function getClass(): string
24+
{
25+
return PDOStatement::class;
26+
}
27+
28+
public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool
29+
{
30+
return 'setfetchmode' === strtolower($methodReflection->getName());
31+
}
32+
33+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
34+
{
35+
$this->typeSpecifier = $typeSpecifier;
36+
}
37+
38+
public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
39+
{
40+
// keep original param name because named-parameters
41+
$methodCall = $node;
42+
$statementType = $scope->getType($methodCall->var);
43+
44+
if ($statementType instanceof GenericObjectType) {
45+
$reducedType = $this->reduceType($methodCall, $statementType, $scope);
46+
47+
if (null !== $reducedType) {
48+
return $this->typeSpecifier->create($methodCall->var, $reducedType, TypeSpecifierContext::createTruthy(), true);
49+
}
50+
}
51+
52+
return new SpecifiedTypes();
53+
}
54+
55+
private function reduceType(MethodCall $methodCall, GenericObjectType $statementType, Scope $scope): ?GenericObjectType
56+
{
57+
$args = $methodCall->getArgs();
58+
59+
if (\count($args) < 1) {
60+
return null;
61+
}
62+
63+
$pdoStatementReflection = new PdoStatementReflection();
64+
65+
$fetchModeType = $scope->getType($args[0]->value);
66+
$fetchType = $pdoStatementReflection->getFetchType($fetchModeType);
67+
if (null === $fetchType) {
68+
return null;
69+
}
70+
71+
return $pdoStatementReflection->modifyGenericStatement($statementType, $fetchType);
72+
}
73+
}

src/PdoReflection/PdoStatementReflection.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,10 @@ public function getFetchType(Type $fetchModeType): ?int
8787
*/
8888
public function createGenericStatement(iterable $queryStrings, int $reflectionFetchType): ?Type
8989
{
90+
$queryReflection = new QueryReflection();
9091
$genericObjects = [];
9192

9293
foreach ($queryStrings as $queryString) {
93-
$queryReflection = new QueryReflection();
9494
$bothType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_BOTH);
9595

9696
if ($bothType) {
@@ -110,6 +110,23 @@ public function createGenericStatement(iterable $queryStrings, int $reflectionFe
110110
return null;
111111
}
112112

113+
/**
114+
* @param QueryReflector::FETCH_TYPE* $fetchType
115+
*/
116+
public function modifyGenericStatement(GenericObjectType $statementType, int $fetchType): ?GenericObjectType
117+
{
118+
$genericTypes = $statementType->getTypes();
119+
120+
if (2 !== \count($genericTypes)) {
121+
return null;
122+
}
123+
124+
$bothType = $genericTypes[1];
125+
$rowTypeInFetchMode = $this->reduceStatementResultType($bothType, $fetchType);
126+
127+
return new GenericObjectType(PDOStatement::class, [$rowTypeInFetchMode, $bothType]);
128+
}
129+
113130
/**
114131
* @param QueryReflector::FETCH_TYPE* $fetchType
115132
*/

tests/default/DbaInferenceTest.php

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

33
namespace staabm\PHPStanDba\Tests;
44

5+
use Composer\InstalledVersions;
6+
use Composer\Semver\VersionParser;
57
use PHPStan\Testing\TypeInferenceTestCase;
68

79
class DbaInferenceTest extends TypeInferenceTestCase
@@ -46,6 +48,11 @@ public function dataFileAsserts(): iterable
4648
}
4749

4850
yield from $this->gatherAssertTypes(__DIR__.'/data/bug254.php');
51+
52+
if (InstalledVersions::satisfies(new VersionParser(), 'phpstan/phpstan', '^1.4.7')) {
53+
yield from $this->gatherAssertTypes(__DIR__.'/data/pdo-stmt-set-fetch-mode.php');
54+
}
55+
4956
yield from $this->gatherAssertTypes(__DIR__.'/data/pdo-union-result.php');
5057
yield from $this->gatherAssertTypes(__DIR__.'/data/mysqli-union-result.php');
5158
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace PdoStmtFetchModeTest;
4+
5+
use PDO;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
public function setFetchModeNum(PDO $pdo)
11+
{
12+
$bothType = ', array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}';
13+
14+
$query = 'SELECT email, adaid FROM ada';
15+
$stmt = $pdo->query($query);
16+
assertType('PDOStatement<array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}'.$bothType.'>', $stmt);
17+
18+
$stmt->setFetchMode(PDO::FETCH_NUM);
19+
assertType('PDOStatement<array{string, int<0, 4294967295>}'.$bothType.'>', $stmt);
20+
21+
$result = $stmt->fetch(PDO::FETCH_NUM);
22+
assertType('array{string, int<0, 4294967295>}|false', $result);
23+
}
24+
25+
public function setFetchModeAssoc(PDO $pdo)
26+
{
27+
$bothType = ', array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}';
28+
29+
$query = 'SELECT email, adaid FROM ada';
30+
$stmt = $pdo->query($query);
31+
assertType('PDOStatement<array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}'.$bothType.'>', $stmt);
32+
33+
$stmt->setFetchMode(PDO::FETCH_ASSOC);
34+
assertType('PDOStatement<array{email: string, adaid: int<0, 4294967295>}'.$bothType.'>', $stmt);
35+
36+
$result = $stmt->fetch(PDO::FETCH_ASSOC);
37+
assertType('array{email: string, adaid: int<0, 4294967295>}|false', $result);
38+
}
39+
40+
public function setFetchModeOnQuery(PDO $pdo)
41+
{
42+
$bothType = ', array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}';
43+
44+
$query = 'SELECT email, adaid FROM ada';
45+
$stmt = $pdo->query($query, PDO::FETCH_NUM);
46+
assertType('PDOStatement<array{string, int<0, 4294967295>}'.$bothType.'>', $stmt);
47+
48+
$stmt->setFetchMode(PDO::FETCH_ASSOC);
49+
assertType('PDOStatement<array{email: string, adaid: int<0, 4294967295>}'.$bothType.'>', $stmt);
50+
51+
$result = $stmt->fetch(PDO::FETCH_NUM);
52+
assertType('array{string, int<0, 4294967295>}|false', $result);
53+
}
54+
}

0 commit comments

Comments
 (0)