diff --git a/config/extensions.neon b/config/extensions.neon index 0d215ded7..9651f8a7b 100644 --- a/config/extensions.neon +++ b/config/extensions.neon @@ -49,6 +49,11 @@ services: tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: staabm\PHPStanDba\Extensions\PdoStatementFetchObjectDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - class: staabm\PHPStanDba\Extensions\PdoStatementColumnCountDynamicReturnTypeExtension tags: diff --git a/src/Extensions/PdoStatementFetchDynamicReturnTypeExtension.php b/src/Extensions/PdoStatementFetchDynamicReturnTypeExtension.php index 1f46a9d70..9c8a4ee1c 100644 --- a/src/Extensions/PdoStatementFetchDynamicReturnTypeExtension.php +++ b/src/Extensions/PdoStatementFetchDynamicReturnTypeExtension.php @@ -10,9 +10,11 @@ use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; @@ -29,9 +31,15 @@ final class PdoStatementFetchDynamicReturnTypeExtension implements DynamicMethod */ private $phpVersion; - public function __construct(PhpVersion $phpVersion) + /** + * @var ReflectionProvider + */ + private $reflectionProvider; + + public function __construct(PhpVersion $phpVersion, ReflectionProvider $reflectionProvider) { $this->phpVersion = $phpVersion; + $this->reflectionProvider = $reflectionProvider; } public function getClass(): string @@ -91,6 +99,25 @@ private function inferType(MethodReflection $methodReflection, MethodCall $metho } $rowType = $pdoStatementReflection->getColumnRowType($statementType, $columnIndex); + } elseif (QueryReflector::FETCH_TYPE_CLASS === $fetchType) { + $className = 'stdClass'; + + if (\count($args) > 1) { + $classStringType = $scope->getType($args[1]->value); + if ($classStringType instanceof ConstantStringType) { + $className = $classStringType->getValue(); + } else { + return null; + } + } + + if (!$this->reflectionProvider->hasClass($className)) { + return null; + } + + $classString = $this->reflectionProvider->getClass($className)->getName(); + + $rowType = $pdoStatementReflection->getClassRowType($statementType, $classString); } else { $rowType = $pdoStatementReflection->getRowType($statementType, $fetchType); } diff --git a/src/Extensions/PdoStatementFetchObjectDynamicReturnTypeExtension.php b/src/Extensions/PdoStatementFetchObjectDynamicReturnTypeExtension.php new file mode 100644 index 000000000..8d3c55f04 --- /dev/null +++ b/src/Extensions/PdoStatementFetchObjectDynamicReturnTypeExtension.php @@ -0,0 +1,78 @@ +reflectionProvider = $reflectionProvider; + } + + public function getClass(): string + { + return PDOStatement::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return \in_array($methodReflection->getName(), ['fetchObject'], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + + $resultType = $this->inferType($methodReflection, $methodCall, $scope); + if (null !== $resultType) { + $returnType = $resultType; + } + + return $returnType; + } + + private function inferType(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + + $className = 'stdClass'; + + if (\count($args) >= 1) { + $classStringType = $scope->getType($args[0]->value); + if ($classStringType instanceof ConstantStringType) { + $className = $classStringType->getValue(); + } else { + return null; + } + } + + if (!$this->reflectionProvider->hasClass($className)) { + // XXX should we return NEVER or FALSE on unknown classes? + return null; + } + + $classString = $this->reflectionProvider->getClass($className)->getName(); + + return TypeCombinator::union(new ObjectType($classString), new ConstantBooleanType(false)); + } +} diff --git a/src/PdoReflection/PdoStatementReflection.php b/src/PdoReflection/PdoStatementReflection.php index be3d43ad4..9dd3f47f9 100644 --- a/src/PdoReflection/PdoStatementReflection.php +++ b/src/PdoReflection/PdoStatementReflection.php @@ -14,6 +14,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; @@ -62,7 +63,9 @@ public function getFetchType(Type $fetchModeType): ?int return null; } - if (PDO::FETCH_KEY_PAIR === $fetchModeType->getValue()) { + if (PDO::FETCH_CLASS === $fetchModeType->getValue()) { + return QueryReflector::FETCH_TYPE_CLASS; + } elseif (PDO::FETCH_KEY_PAIR === $fetchModeType->getValue()) { return QueryReflector::FETCH_TYPE_KEY_VALUE; } elseif (PDO::FETCH_ASSOC === $fetchModeType->getValue()) { return QueryReflector::FETCH_TYPE_ASSOC; @@ -154,6 +157,14 @@ public function getColumnRowType(Type $statementType, int $columnIndex): ?Type return null; } + /** + * @param class-string $className + */ + public function getClassRowType(Type $statementType, string $className): ?Type + { + return new ObjectType($className); + } + /** * @param QueryReflector::FETCH_TYPE* $fetchType */ diff --git a/src/QueryReflection/QueryReflector.php b/src/QueryReflection/QueryReflector.php index 5fccb06e8..38793e161 100644 --- a/src/QueryReflection/QueryReflector.php +++ b/src/QueryReflection/QueryReflector.php @@ -13,8 +13,8 @@ interface QueryReflector public const FETCH_TYPE_NUMERIC = 4; public const FETCH_TYPE_BOTH = 5; public const FETCH_TYPE_KEY_VALUE = 6; - - public const FETCH_TYPE_COLUMN = 50; + public const FETCH_TYPE_COLUMN = 7; + public const FETCH_TYPE_CLASS = 8; public function validateQueryString(string $queryString): ?Error; diff --git a/tests/default/Fixture/MyRowClass.php b/tests/default/Fixture/MyRowClass.php new file mode 100644 index 000000000..f768907f0 --- /dev/null +++ b/tests/default/Fixture/MyRowClass.php @@ -0,0 +1,7 @@ +fetchAll(PDO::FETCH_KEY_PAIR); assertType('array>>', $all); + $all = $stmt->fetchAll(PDO::FETCH_CLASS, MyRowClass::class); + assertType('array', $all); + + $all = $stmt->fetchAll(PDO::FETCH_CLASS); + assertType('array', $all); + // not yet supported fetch types $all = $stmt->fetchAll(PDO::FETCH_OBJ); assertType('array', $all); // XXX since php8 this cannot return false @@ -78,6 +85,12 @@ public function fetch(PDO $pdo) $all = $stmt->fetch(PDO::FETCH_KEY_PAIR); assertType('array>|false', $all); + $all = $stmt->fetchObject(MyRowClass::class); + assertType('staabm\PHPStanDba\Tests\Fixture\MyRowClass|false', $all); + + $all = $stmt->fetchObject(); + assertType('stdClass|false', $all); + // not yet supported fetch types $all = $stmt->fetch(PDO::FETCH_OBJ); assertType('mixed', $all); diff --git a/tests/rules/config/.phpstan-dba-mysqli.cache b/tests/rules/config/.phpstan-dba-mysqli.cache index 4fd72dad7..e8565ec04 100644 --- a/tests/rules/config/.phpstan-dba-mysqli.cache +++ b/tests/rules/config/.phpstan-dba-mysqli.cache @@ -972,7 +972,7 @@ array ( 'result' => array ( - 5 => NULL, + 3 => NULL, ), ), 'SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE gesperrt=1' =>