Skip to content

Commit

Permalink
PDO: Added PDO::FETCH_CLASS support on fetch*(), fetchObject() (#…
Browse files Browse the repository at this point in the history
…304)

Co-authored-by: Markus Staab <m.staab@complex-it.de>
  • Loading branch information
staabm and clxmstaab authored Feb 19, 2022
1 parent 1e28adc commit 29dddd0
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 5 deletions.
5 changes: 5 additions & 0 deletions config/extensions.neon
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ services:
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension

-
class: staabm\PHPStanDba\Extensions\PdoStatementFetchObjectDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension

-
class: staabm\PHPStanDba\Extensions\PdoStatementColumnCountDynamicReturnTypeExtension
tags:
Expand Down
29 changes: 28 additions & 1 deletion src/Extensions/PdoStatementFetchDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace staabm\PHPStanDba\Extensions;

use PDOStatement;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

final class PdoStatementFetchObjectDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
/**
* @var ReflectionProvider
*/
private $reflectionProvider;

public function __construct(ReflectionProvider $reflectionProvider)
{
$this->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));
}
}
13 changes: 12 additions & 1 deletion src/PdoReflection/PdoStatementReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down
4 changes: 2 additions & 2 deletions src/QueryReflection/QueryReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
7 changes: 7 additions & 0 deletions tests/default/Fixture/MyRowClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace staabm\PHPStanDba\Tests\Fixture;

class MyRowClass
{
}
13 changes: 13 additions & 0 deletions tests/default/data/pdo-stmt-fetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PDO;
use function PHPStan\Testing\assertType;
use staabm\PHPStanDba\Tests\Fixture\MyRowClass;

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

$all = $stmt->fetchAll(PDO::FETCH_CLASS, MyRowClass::class);
assertType('array<int, staabm\PHPStanDba\Tests\Fixture\MyRowClass>', $all);

$all = $stmt->fetchAll(PDO::FETCH_CLASS);
assertType('array<int, stdClass>', $all);

// not yet supported fetch types
$all = $stmt->fetchAll(PDO::FETCH_OBJ);
assertType('array', $all); // XXX since php8 this cannot return false
Expand Down Expand Up @@ -78,6 +85,12 @@ public function fetch(PDO $pdo)
$all = $stmt->fetch(PDO::FETCH_KEY_PAIR);
assertType('array<string, int<0, 4294967295>>|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);
Expand Down
2 changes: 1 addition & 1 deletion tests/rules/config/.phpstan-dba-mysqli.cache

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 29dddd0

Please sign in to comment.