Skip to content

Commit 4d44219

Browse files
staabmclxmstaab
andauthored
put both-type and fetch-type into PDOStatement generic (#268)
Co-authored-by: Markus Staab <[email protected]>
1 parent 9a3c964 commit 4d44219

31 files changed

+8278
-1454
lines changed

.phpstan-dba.cache

Lines changed: 4400 additions & 79 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@
4646
"phpstan analyse -c phpstan.neon.dist",
4747
"phpstan analyse -c tests/default/config/phpstan.neon.dist",
4848
"phpstan analyse -c tests/stringify/config/phpstan.neon.dist",
49-
"phpstan analyse -c tests/defaultFetchAssoc/config/phpstan.neon.dist"
49+
"phpstan analyse -c tests/defaultFetchAssoc/config/phpstan.neon.dist",
50+
"phpstan analyse -c tests/defaultFetchNumeric/config/phpstan.neon.dist"
5051
],
5152
"phpunit": [
5253
"phpunit -c tests/default/config/phpunit.xml",
5354
"phpunit -c tests/stringify/config/phpunit.xml",
54-
"phpunit -c tests/defaultFetchAssoc/config/phpunit.xml"
55+
"phpunit -c tests/defaultFetchAssoc/config/phpunit.xml",
56+
"phpunit -c tests/defaultFetchNumeric/config/phpunit.xml"
5557
]
5658
},
5759
"config": {

config/PdoStatement.stub

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<?php
22

33
/**
4-
* @template TValue
5-
* @implements Traversable<int, TValue>
6-
* @implements IteratorAggregate<int, TValue>
4+
* @template TRowTypeInFetchMode
5+
* @template TRowTypeBoth
6+
* @implements Traversable<int, TRowTypeInFetchMode>
7+
* @implements IteratorAggregate<int, TRowTypeInFetchMode>
78
* @link https://php.net/manual/en/class.pdostatement.php
89
*/
910
class PDOStatement implements Traversable, IteratorAggregate

src/Extensions/PdoPrepareDynamicReturnTypeExtension.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace staabm\PHPStanDba\Extensions;
66

77
use PDO;
8-
use PDOStatement;
98
use PhpParser\Node\Expr;
109
use PhpParser\Node\Expr\MethodCall;
1110
use PHPStan\Analyser\Scope;
@@ -14,10 +13,10 @@
1413
use PHPStan\Reflection\ParametersAcceptorSelector;
1514
use PHPStan\Type\Constant\ConstantBooleanType;
1615
use PHPStan\Type\DynamicMethodReturnTypeExtension;
17-
use PHPStan\Type\Generic\GenericObjectType;
1816
use PHPStan\Type\MixedType;
1917
use PHPStan\Type\Type;
2018
use PHPStan\Type\TypeCombinator;
19+
use staabm\PHPStanDba\PdoReflection\PdoStatementReflection;
2120
use staabm\PHPStanDba\QueryReflection\QueryReflection;
2221

2322
final class PdoPrepareDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
@@ -76,11 +75,8 @@ private function inferType(Expr $queryExpr, Scope $scope): ?Type
7675
}
7776

7877
$reflectionFetchType = QueryReflection::getRuntimeConfiguration()->getDefaultFetchMode();
79-
$resultType = $queryReflection->getResultType($queryString, $reflectionFetchType);
80-
if ($resultType) {
81-
return new GenericObjectType(PDOStatement::class, [$resultType]);
82-
}
78+
$pdoStatementReflection = new PdoStatementReflection();
8379

84-
return null;
80+
return $pdoStatementReflection->createGenericStatement($queryString, $reflectionFetchType);
8581
}
8682
}

src/Extensions/PdoQueryDynamicReturnTypeExtension.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace staabm\PHPStanDba\Extensions;
66

77
use PDO;
8-
use PDOStatement;
98
use PhpParser\Node\Expr;
109
use PhpParser\Node\Expr\MethodCall;
1110
use PHPStan\Analyser\Scope;
@@ -14,7 +13,6 @@
1413
use PHPStan\Reflection\ParametersAcceptorSelector;
1514
use PHPStan\Type\Constant\ConstantBooleanType;
1615
use PHPStan\Type\DynamicMethodReturnTypeExtension;
17-
use PHPStan\Type\Generic\GenericObjectType;
1816
use PHPStan\Type\MixedType;
1917
use PHPStan\Type\Type;
2018
use PHPStan\Type\TypeCombinator;
@@ -93,11 +91,8 @@ private function inferType(MethodCall $methodCall, Expr $queryExpr, Scope $scope
9391
return null;
9492
}
9593

96-
$resultType = $queryReflection->getResultType($queryString, $reflectionFetchType);
97-
if ($resultType) {
98-
return new GenericObjectType(PDOStatement::class, [$resultType]);
99-
}
94+
$pdoStatementReflection = new PdoStatementReflection();
10095

101-
return null;
96+
return $pdoStatementReflection->createGenericStatement($queryString, $reflectionFetchType);
10297
}
10398
}

src/Extensions/PdoStatementColumnCountDynamicReturnTypeExtension.php

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1515
use PHPStan\Type\Generic\GenericObjectType;
1616
use PHPStan\Type\Type;
17+
use staabm\PHPStanDba\PdoReflection\PdoStatementReflection;
18+
use staabm\PHPStanDba\QueryReflection\QueryReflector;
1719

1820
final class PdoStatementColumnCountDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
1921
{
@@ -29,21 +31,15 @@ public function isMethodSupported(MethodReflection $methodReflection): bool
2931

3032
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
3133
{
34+
$pdoStatementReflection = new PdoStatementReflection();
3235
$defaultReturn = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
3336

3437
$statementType = $scope->getType($methodCall->var);
3538

3639
if ($statementType instanceof GenericObjectType) {
37-
$genericTypes = $statementType->getTypes();
38-
39-
if (1 !== \count($genericTypes)) {
40-
return $defaultReturn;
41-
}
42-
43-
$resultType = $genericTypes[0];
44-
45-
if ($resultType instanceof ConstantArrayType) {
46-
return new ConstantIntegerType(\count($resultType->getKeyTypes()));
40+
$rowType = $pdoStatementReflection->getRowType($statementType, QueryReflector::FETCH_TYPE_NUMERIC);
41+
if ($rowType instanceof ConstantArrayType) {
42+
return new ConstantIntegerType(\count($rowType->getKeyTypes()));
4743
}
4844
}
4945

src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
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;
1716
use PHPStan\Type\Type;
1817
use staabm\PHPStanDba\PdoReflection\PdoStatementReflection;
@@ -73,12 +72,7 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa
7372
}
7473

7574
$reflectionFetchType = QueryReflection::getRuntimeConfiguration()->getDefaultFetchMode();
76-
$resultType = $queryReflection->getResultType($queryString, $reflectionFetchType);
7775

78-
if ($resultType) {
79-
return new GenericObjectType(PDOStatement::class, [$resultType]);
80-
}
81-
82-
return null;
76+
return $stmtReflection->createGenericStatement($queryString, $reflectionFetchType);
8377
}
8478
}

src/Extensions/PdoStatementFetchDynamicReturnTypeExtension.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Type\ArrayType;
1414
use PHPStan\Type\Constant\ConstantBooleanType;
1515
use PHPStan\Type\DynamicMethodReturnTypeExtension;
16+
use PHPStan\Type\Generic\GenericObjectType;
1617
use PHPStan\Type\IntegerType;
1718
use PHPStan\Type\MixedType;
1819
use PHPStan\Type\Type;
@@ -65,6 +66,9 @@ private function inferType(MethodReflection $methodReflection, MethodCall $metho
6566
$pdoStatementReflection = new PdoStatementReflection();
6667

6768
$statementType = $scope->getType($methodCall->var);
69+
if (!$statementType instanceof GenericObjectType) {
70+
return null;
71+
}
6872

6973
$fetchType = QueryReflection::getRuntimeConfiguration()->getDefaultFetchMode();
7074
if (\count($args) > 0) {
@@ -76,19 +80,19 @@ private function inferType(MethodReflection $methodReflection, MethodCall $metho
7680
}
7781
}
7882

79-
$resultType = $pdoStatementReflection->getStatementResultType($statementType, $fetchType);
80-
if (null === $resultType) {
83+
$rowType = $pdoStatementReflection->getRowType($statementType, $fetchType);
84+
if (null === $rowType) {
8185
return null;
8286
}
8387

8488
if ('fetchAll' === $methodReflection->getName()) {
85-
return new ArrayType(new IntegerType(), $resultType);
89+
return new ArrayType(new IntegerType(), $rowType);
8690
}
8791

8892
if (QueryReflection::getRuntimeConfiguration()->throwsPdoExceptions($this->phpVersion)) {
89-
return $resultType;
93+
return $rowType;
9094
}
9195

92-
return new UnionType([$resultType, new ConstantBooleanType(false)]);
96+
return new UnionType([$rowType, new ConstantBooleanType(false)]);
9397
}
9498
}

src/PdoReflection/PdoStatementReflection.php

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace staabm\PHPStanDba\PdoReflection;
66

77
use PDO;
8+
use PDOStatement;
89
use PhpParser\Node\Expr;
910
use PhpParser\Node\Expr\MethodCall;
1011
use PHPStan\Type\Constant\ConstantArrayType;
@@ -35,6 +36,8 @@ public function findPrepareQueryStringExpression(MethodCall $methodCall): ?Expr
3536
}
3637

3738
/**
39+
* Turns a PDO::FETCH_* parameter-type into a QueryReflector::FETCH_TYPE* constant.
40+
*
3841
* @return QueryReflector::FETCH_TYPE*|null
3942
*/
4043
public function getFetchType(Type $fetchModeType): ?int
@@ -55,46 +58,52 @@ public function getFetchType(Type $fetchModeType): ?int
5558
}
5659

5760
/**
58-
* @param QueryReflector::FETCH_TYPE* $fetchType
61+
* @param QueryReflector::FETCH_TYPE* $reflectionFetchType
5962
*/
60-
public function getStatementResultType(Type $statementType, int $fetchType): ?Type
63+
public function createGenericStatement(string $queryString, int $reflectionFetchType): ?GenericObjectType
6164
{
62-
if (!$statementType instanceof GenericObjectType) {
63-
return null;
64-
}
65+
$queryReflection = new QueryReflection();
66+
$bothType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_BOTH);
6567

66-
$genericTypes = $statementType->getTypes();
67-
if (1 !== \count($genericTypes)) {
68-
return null;
68+
if ($bothType) {
69+
$rowTypeInFetchMode = $this->reduceStatementResultType($bothType, $reflectionFetchType);
70+
71+
return new GenericObjectType(PDOStatement::class, [$rowTypeInFetchMode, $bothType]);
6972
}
7073

71-
$resultType = $genericTypes[0];
74+
return null;
75+
}
7276

73-
// turn ASSOC typed statement into a NUMERIC one
74-
$defaultFetchType = QueryReflection::getRuntimeConfiguration()->getDefaultFetchMode();
75-
if (QueryReflector::FETCH_TYPE_ASSOC === $defaultFetchType && QueryReflector::FETCH_TYPE_NUMERIC === $fetchType &&
76-
$resultType instanceof ConstantArrayType && \count($resultType->getValueTypes()) > 0) {
77-
$builder = ConstantArrayTypeBuilder::createEmpty();
77+
/**
78+
* @param QueryReflector::FETCH_TYPE* $fetchType
79+
*/
80+
public function getRowType(GenericObjectType $statementType, int $fetchType): ?Type
81+
{
82+
$genericTypes = $statementType->getTypes();
7883

79-
$valueTypes = $resultType->getValueTypes();
84+
if (2 !== \count($genericTypes)) {
85+
return null;
86+
}
8087

81-
$i = 0;
82-
foreach ($valueTypes as $valueType) {
83-
$builder->setOffsetValueType(new ConstantIntegerType($i), $valueType);
84-
++$i;
85-
}
88+
$bothType = $genericTypes[1];
8689

87-
return $builder->getArray();
88-
}
90+
return $this->reduceStatementResultType($bothType, $fetchType);
91+
}
8992

93+
/**
94+
* @param QueryReflector::FETCH_TYPE* $fetchType
95+
*/
96+
private function reduceStatementResultType(Type $bothType, int $fetchType): Type
97+
{
9098
// turn a BOTH typed statement into either NUMERIC or ASSOC
91-
if (QueryReflector::FETCH_TYPE_BOTH === $defaultFetchType &&
99+
if (
92100
(QueryReflector::FETCH_TYPE_NUMERIC === $fetchType || QueryReflector::FETCH_TYPE_ASSOC === $fetchType) &&
93-
$resultType instanceof ConstantArrayType && \count($resultType->getValueTypes()) > 0) {
101+
$bothType instanceof ConstantArrayType && \count($bothType->getValueTypes()) > 0
102+
) {
94103
$builder = ConstantArrayTypeBuilder::createEmpty();
95104

96-
$keyTypes = $resultType->getKeyTypes();
97-
$valueTypes = $resultType->getValueTypes();
105+
$keyTypes = $bothType->getKeyTypes();
106+
$valueTypes = $bothType->getValueTypes();
98107

99108
foreach ($keyTypes as $i => $keyType) {
100109
if (QueryReflector::FETCH_TYPE_NUMERIC === $fetchType && $keyType instanceof ConstantIntegerType) {
@@ -107,6 +116,7 @@ public function getStatementResultType(Type $statementType, int $fetchType): ?Ty
107116
return $builder->getArray();
108117
}
109118

110-
return $resultType;
119+
// not yet supported fetch type - or $fetchType == BOTH
120+
return $bothType;
111121
}
112122
}

src/QueryReflection/RuntimeConfiguration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function errorMode(string $mode): self
6161
* Defines the PDO default fetch mode.
6262
* This might be necessary in case you are using `\PDO::ATTR_DEFAULT_FETCH_MODE`.
6363
*
64-
* @param QueryReflector::FETCH_TYPE_BOTH|QueryReflector::FETCH_TYPE_ASSOC $mode
64+
* @param QueryReflector::FETCH_TYPE_* $mode
6565
*/
6666
public function defaultFetchMode(int $mode): self
6767
{

0 commit comments

Comments
 (0)