Skip to content

Commit 665fd2f

Browse files
staabmclxmstaab
andauthored
added PdoStatementObjectType (#322)
Co-authored-by: Markus Staab <[email protected]>
1 parent 6fb6b07 commit 665fd2f

File tree

15 files changed

+188
-254
lines changed

15 files changed

+188
-254
lines changed

config/PdoStatement.stub

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

33
/**
44
* @template TRowTypeInFetchMode
5-
* @template TRowTypeBoth
5+
66
* @implements Traversable<int, TRowTypeInFetchMode>
77
* @implements IteratorAggregate<int, TRowTypeInFetchMode>
88
* @link https://php.net/manual/en/class.pdostatement.php

src/Extensions/PdoStatementSetFetchModeTypeSpecifyingExtension.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
use PHPStan\Analyser\TypeSpecifierAwareExtension;
1313
use PHPStan\Analyser\TypeSpecifierContext;
1414
use PHPStan\Reflection\MethodReflection;
15-
use PHPStan\Type\Generic\GenericObjectType;
1615
use PHPStan\Type\MethodTypeSpecifyingExtension;
16+
use staabm\PHPStanDba\PdoReflection\PdoStatementObjectType;
1717
use staabm\PHPStanDba\PdoReflection\PdoStatementReflection;
1818

1919
final class PdoStatementSetFetchModeTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
@@ -41,7 +41,7 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod
4141
$methodCall = $node;
4242
$statementType = $scope->getType($methodCall->var);
4343

44-
if ($statementType instanceof GenericObjectType) {
44+
if ($statementType instanceof PdoStatementObjectType) {
4545
$reducedType = $this->reduceType($methodCall, $statementType, $scope);
4646

4747
if (null !== $reducedType) {
@@ -52,7 +52,7 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod
5252
return new SpecifiedTypes();
5353
}
5454

55-
private function reduceType(MethodCall $methodCall, GenericObjectType $statementType, Scope $scope): ?GenericObjectType
55+
private function reduceType(MethodCall $methodCall, PdoStatementObjectType $statementType, Scope $scope): ?PdoStatementObjectType
5656
{
5757
$args = $methodCall->getArgs();
5858

@@ -68,6 +68,6 @@ private function reduceType(MethodCall $methodCall, GenericObjectType $statement
6868
return null;
6969
}
7070

71-
return $pdoStatementReflection->modifyGenericStatement($statementType, $fetchType);
71+
return $statementType->newWithFetchType($fetchType);
7272
}
7373
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace staabm\PHPStanDba\PdoReflection;
6+
7+
use PDOStatement;
8+
use PHPStan\Type\ArrayType;
9+
use PHPStan\Type\Constant\ConstantArrayType;
10+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
11+
use PHPStan\Type\Constant\ConstantIntegerType;
12+
use PHPStan\Type\Constant\ConstantStringType;
13+
use PHPStan\Type\Generic\GenericObjectType;
14+
use PHPStan\Type\IntegerType;
15+
use PHPStan\Type\ObjectType;
16+
use PHPStan\Type\Type;
17+
use staabm\PHPStanDba\QueryReflection\QueryReflector;
18+
19+
class PdoStatementObjectType extends GenericObjectType
20+
{
21+
private Type $bothType;
22+
23+
/**
24+
* @param QueryReflector::FETCH_TYPE* $fetchType
25+
*/
26+
public function __construct(Type $bothType, int $fetchType)
27+
{
28+
$this->bothType = $bothType;
29+
30+
$rowTypeInFetchMode = $this->reduceBothType($bothType, $fetchType);
31+
32+
parent::__construct(PDOStatement::class, [$rowTypeInFetchMode]);
33+
}
34+
35+
public function getRowType(): Type
36+
{
37+
$genericTypes = $this->getTypes();
38+
39+
return $genericTypes[0];
40+
}
41+
42+
/**
43+
* @param QueryReflector::FETCH_TYPE* $fetchType
44+
*/
45+
public function newWithFetchType(int $fetchType): self
46+
{
47+
return new self($this->bothType, $fetchType);
48+
}
49+
50+
/**
51+
* @param QueryReflector::FETCH_TYPE* $fetchType
52+
*/
53+
private function reduceBothType(Type $bothType, int $fetchType): Type
54+
{
55+
if (!$bothType instanceof ConstantArrayType) {
56+
return $bothType;
57+
}
58+
59+
if (\count($bothType->getValueTypes()) <= 0) {
60+
return $bothType;
61+
}
62+
63+
// turn a BOTH typed statement into either NUMERIC or ASSOC
64+
if (
65+
QueryReflector::FETCH_TYPE_NUMERIC === $fetchType || QueryReflector::FETCH_TYPE_ASSOC === $fetchType
66+
) {
67+
$builder = ConstantArrayTypeBuilder::createEmpty();
68+
69+
$keyTypes = $bothType->getKeyTypes();
70+
$valueTypes = $bothType->getValueTypes();
71+
72+
foreach ($keyTypes as $i => $keyType) {
73+
if (QueryReflector::FETCH_TYPE_NUMERIC === $fetchType && $keyType instanceof ConstantIntegerType) {
74+
$builder->setOffsetValueType($keyType, $valueTypes[$i]);
75+
} elseif (QueryReflector::FETCH_TYPE_ASSOC === $fetchType && $keyType instanceof ConstantStringType) {
76+
$builder->setOffsetValueType($keyType, $valueTypes[$i]);
77+
}
78+
}
79+
80+
return $builder->getArray();
81+
}
82+
83+
if (QueryReflector::FETCH_TYPE_CLASS === $fetchType) {
84+
return new ArrayType(new IntegerType(), new ObjectType('stdClass'));
85+
}
86+
87+
// both types contains numeric and string keys, therefore the count is doubled
88+
if (QueryReflector::FETCH_TYPE_KEY_VALUE === $fetchType && \count($bothType->getValueTypes()) >= 4) {
89+
$valueTypes = $bothType->getValueTypes();
90+
91+
return new ArrayType($valueTypes[0], $valueTypes[2]);
92+
}
93+
94+
// not yet supported fetch type - or $fetchType == BOTH
95+
return $bothType;
96+
}
97+
}

src/PdoReflection/PdoStatementReflection.php

Lines changed: 3 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,10 @@
55
namespace staabm\PHPStanDba\PdoReflection;
66

77
use PDO;
8-
use PDOStatement;
98
use PhpParser\Node\Expr;
109
use PhpParser\Node\Expr\MethodCall;
11-
use PHPStan\Type\ArrayType;
1210
use PHPStan\Type\Constant\ConstantArrayType;
13-
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1411
use PHPStan\Type\Constant\ConstantIntegerType;
15-
use PHPStan\Type\Constant\ConstantStringType;
16-
use PHPStan\Type\Generic\GenericObjectType;
17-
use PHPStan\Type\IntegerType;
1812
use PHPStan\Type\ObjectType;
1913
use PHPStan\Type\Type;
2014
use PHPStan\Type\TypeCombinator;
@@ -94,9 +88,7 @@ public function createGenericStatement(iterable $queryStrings, int $reflectionFe
9488
$bothType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_BOTH);
9589

9690
if ($bothType) {
97-
$rowTypeInFetchMode = $this->reduceStatementResultType($bothType, $reflectionFetchType);
98-
99-
$genericObjects[] = new GenericObjectType(PDOStatement::class, [$rowTypeInFetchMode, $bothType]);
91+
$genericObjects[] = new PdoStatementObjectType($bothType, $reflectionFetchType);
10092
}
10193
}
10294

@@ -110,23 +102,6 @@ public function createGenericStatement(iterable $queryStrings, int $reflectionFe
110102
return null;
111103
}
112104

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-
130105
/**
131106
* @param QueryReflector::FETCH_TYPE* $fetchType
132107
*/
@@ -146,16 +121,8 @@ public function getRowType(Type $statementType, int $fetchType): ?Type
146121
return TypeCombinator::union(...$rowTypes);
147122
}
148123

149-
if ($statementType instanceof GenericObjectType) {
150-
$genericTypes = $statementType->getTypes();
151-
152-
if (2 !== \count($genericTypes)) {
153-
return null;
154-
}
155-
156-
$bothType = $genericTypes[1];
157-
158-
return $this->reduceStatementResultType($bothType, $fetchType);
124+
if ($statementType instanceof PdoStatementObjectType) {
125+
return $statementType->newWithFetchType($fetchType)->getRowType();
159126
}
160127

161128
return null;
@@ -182,52 +149,4 @@ public function getClassRowType(Type $statementType, string $className): ?Type
182149
{
183150
return new ObjectType($className);
184151
}
185-
186-
/**
187-
* @param QueryReflector::FETCH_TYPE* $fetchType
188-
*/
189-
private function reduceStatementResultType(Type $bothType, int $fetchType): Type
190-
{
191-
if (!$bothType instanceof ConstantArrayType) {
192-
return $bothType;
193-
}
194-
195-
if (\count($bothType->getValueTypes()) <= 0) {
196-
return $bothType;
197-
}
198-
199-
// turn a BOTH typed statement into either NUMERIC or ASSOC
200-
if (
201-
QueryReflector::FETCH_TYPE_NUMERIC === $fetchType || QueryReflector::FETCH_TYPE_ASSOC === $fetchType
202-
) {
203-
$builder = ConstantArrayTypeBuilder::createEmpty();
204-
205-
$keyTypes = $bothType->getKeyTypes();
206-
$valueTypes = $bothType->getValueTypes();
207-
208-
foreach ($keyTypes as $i => $keyType) {
209-
if (QueryReflector::FETCH_TYPE_NUMERIC === $fetchType && $keyType instanceof ConstantIntegerType) {
210-
$builder->setOffsetValueType($keyType, $valueTypes[$i]);
211-
} elseif (QueryReflector::FETCH_TYPE_ASSOC === $fetchType && $keyType instanceof ConstantStringType) {
212-
$builder->setOffsetValueType($keyType, $valueTypes[$i]);
213-
}
214-
}
215-
216-
return $builder->getArray();
217-
}
218-
219-
if (QueryReflector::FETCH_TYPE_CLASS === $fetchType) {
220-
return new ArrayType(new IntegerType(), new ObjectType('stdClass'));
221-
}
222-
223-
// both types contains numeric and string keys, therefore the count is doubled
224-
if (QueryReflector::FETCH_TYPE_KEY_VALUE === $fetchType && \count($bothType->getValueTypes()) >= 4) {
225-
$valueTypes = $bothType->getValueTypes();
226-
227-
return new ArrayType($valueTypes[0], $valueTypes[2]);
228-
}
229-
230-
// not yet supported fetch type - or $fetchType == BOTH
231-
return $bothType;
232-
}
233152
}

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,21 @@ class Foo
99
{
1010
public function supportedFetchTypes(PDO $pdo)
1111
{
12-
$bothType = ', array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}';
13-
1412
// default fetch-type is BOTH
1513
$stmt = $pdo->query('SELECT email, adaid FROM ada');
16-
assertType('PDOStatement<array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}'.$bothType.'>', $stmt);
14+
assertType('PDOStatement<array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}>', $stmt);
1715

1816
$stmt = $pdo->query('SELECT email, adaid FROM ada', PDO::FETCH_NUM);
19-
assertType('PDOStatement<array{string, int<0, 4294967295>}'.$bothType.'>', $stmt);
17+
assertType('PDOStatement<array{string, int<0, 4294967295>}>', $stmt);
2018

2119
$stmt = $pdo->query('SELECT email, adaid FROM ada', PDO::FETCH_ASSOC);
22-
assertType('PDOStatement<array{email: string, adaid: int<0, 4294967295>}'.$bothType.'>', $stmt);
20+
assertType('PDOStatement<array{email: string, adaid: int<0, 4294967295>}>', $stmt);
2321

2422
$stmt = $pdo->query('SELECT email, adaid FROM ada', PDO::FETCH_BOTH);
25-
assertType('PDOStatement<array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}'.$bothType.'>', $stmt);
23+
assertType('PDOStatement<array{email: string, 0: string, adaid: int<0, 4294967295>, 1: int<0, 4294967295>}>', $stmt);
2624

2725
$stmt = $pdo->query('SELECT email, adaid FROM ada', PDO::FETCH_OBJ);
28-
assertType('PDOStatement<array<int, stdClass>'.$bothType.'>', $stmt);
26+
assertType('PDOStatement<array<int, stdClass>>', $stmt);
2927
}
3028

3129
public function unsupportedFetchTypes(PDO $pdo)

0 commit comments

Comments
 (0)