Skip to content

Commit

Permalink
Remove use of PDOStatement stub and and generic PDOStatement result t…
Browse files Browse the repository at this point in the history
…ype (#700)
  • Loading branch information
staabm authored Nov 7, 2024
1 parent e70e5ac commit 436e2e9
Show file tree
Hide file tree
Showing 23 changed files with 906 additions and 331 deletions.
25 changes: 0 additions & 25 deletions config/PdoStatement.stub

This file was deleted.

1 change: 0 additions & 1 deletion config/dba.neon
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ includes:
parameters:
featureToggles:
skipCheckGenericClasses:
- PDOStatement
- Doctrine\DBAL\Result
- Doctrine\DBAL\Statement

Expand Down
1 change: 0 additions & 1 deletion config/stubFiles.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
parameters:
stubFiles:
- DoctrineDbal.stub
- PdoStatement.stub
93 changes: 69 additions & 24 deletions src/PdoReflection/PdoStatementObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
namespace staabm\PHPStanDba\PdoReflection;

use PDOStatement;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\FloatType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
Expand All @@ -21,38 +22,48 @@
use PHPStan\Type\UnionType;
use staabm\PHPStanDba\QueryReflection\QueryReflector;

class PdoStatementObjectType extends GenericObjectType
class PdoStatementObjectType extends ObjectType
{
/**
* @var Type
*/
private $bothType;
private ?Type $bothType;

/**
* @param QueryReflector::FETCH_TYPE* $fetchType
* @var null|QueryReflector::FETCH_TYPE*
*/
public function __construct(Type $bothType, int $fetchType)
{
$this->bothType = $bothType;
private ?int $fetchType;

$rowTypeInFetchMode = $this->reduceBothType($bothType, $fetchType);

parent::__construct(PDOStatement::class, [$rowTypeInFetchMode]);
public function getRowType(): Type
{
if ($this->bothType === null || $this->fetchType === null) {
throw new ShouldNotHappenException();
}
return $this->reduceBothType($this->bothType, $this->fetchType);
}

public function getRowType(): Type
public function getIterableValueType(): Type
{
$genericTypes = $this->getTypes();
return $this->getRowType();
}

return $genericTypes[0];
/**
* @param QueryReflector::FETCH_TYPE* $fetchType
*/
public static function newWithBothAndFetchType(Type $bothType, int $fetchType): self
{
$new = new self(PDOStatement::class);
$new->bothType = $bothType;
$new->fetchType = $fetchType;
return $new;
}

/**
* @param QueryReflector::FETCH_TYPE* $fetchType
*/
public function newWithFetchType(int $fetchType): self
{
return new self($this->bothType, $fetchType);
$new = new self($this->getClassName(), $this->getSubtractedType());
$new->bothType = $this->bothType;
$new->fetchType = $fetchType;
return $new;
}

/**
Expand Down Expand Up @@ -116,23 +127,57 @@ public static function createDefaultType(int $fetchType): Type

switch ($fetchType) {
case QueryReflector::FETCH_TYPE_CLASS:
return new GenericObjectType(PDOStatement::class, [new ObjectType('stdClass')]);
return self::newWithBothAndFetchType(new ObjectType('stdClass'), $fetchType);
case QueryReflector::FETCH_TYPE_KEY_VALUE:
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
$arrayBuilder->setOffsetValueType(new ConstantIntegerType(0), new MixedType());
$arrayBuilder->setOffsetValueType(new ConstantIntegerType(1), new MixedType());

return new GenericObjectType(PDOStatement::class, [$arrayBuilder->getArray()]);
return self::newWithBothAndFetchType($arrayBuilder->getArray(), $fetchType);
case QueryReflector::FETCH_TYPE_NUMERIC:
return new GenericObjectType(PDOStatement::class, [new ArrayType(IntegerRangeType::fromInterval(0, null), $pdoScalar)]);
return self::newWithBothAndFetchType(new ArrayType(IntegerRangeType::fromInterval(0, null), $pdoScalar), $fetchType);
case QueryReflector::FETCH_TYPE_ASSOC:
return new GenericObjectType(PDOStatement::class, [new ArrayType(new StringType(), $pdoScalar)]);
return self::newWithBothAndFetchType(new ArrayType(new StringType(), $pdoScalar), $fetchType);
case QueryReflector::FETCH_TYPE_BOTH:
return new GenericObjectType(PDOStatement::class, [new ArrayType($arrayKey, $pdoScalar)]);
return self::newWithBothAndFetchType(new ArrayType($arrayKey, $pdoScalar), $fetchType);
case QueryReflector::FETCH_TYPE_COLUMN:
return new GenericObjectType(PDOStatement::class, [$pdoScalar]);
return self::newWithBothAndFetchType($pdoScalar, $fetchType);
}

return self::newWithBothAndFetchType(new MixedType(), $fetchType);
}

// differentiate objects based on the local properties,
// to make sure TypeCombinator::union() will not normalize separate objects away.
// this means we need to implement equals() and isSuperTypeOf().
public function equals(Type $type): bool
{
if (
$type instanceof self
&& $type->fetchType !== null
&& $type->bothType !== null
&& $this->bothType !== null
) {
return $type->fetchType === $this->fetchType && $type->bothType->equals($this->bothType);
}

return parent::equals($type);
}

public function isSuperTypeOf(Type $type): TrinaryLogic
{
if (
$type instanceof self
&& $type->fetchType !== null
&& $type->bothType !== null
&& $this->bothType !== null
) {
return TrinaryLogic::createFromBoolean(
$type->fetchType === $this->fetchType
&& $type->bothType->equals($this->bothType)
);
}

return new GenericObjectType(PDOStatement::class, [new MixedType()]);
return parent::isSuperTypeOf($type);
}
}
2 changes: 1 addition & 1 deletion src/PdoReflection/PdoStatementReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function createGenericStatement(iterable $queryStrings, int $reflectionFe
$bothType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_BOTH);

if (null !== $bothType) {
$genericObjects[] = new PdoStatementObjectType($bothType, $reflectionFetchType);
$genericObjects[] = PdoStatementObjectType::newWithBothAndFetchType($bothType, $reflectionFetchType);
}
}

Expand Down
12 changes: 2 additions & 10 deletions tests/default/data/pdo-default-fetch-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class HelloWorld
public function defaultFetchType(PDO $pdo, string $q): void
{
$stmt = $pdo->query($q);
assertType('PDOStatement<array<float|int|string|null>>', $stmt);
foreach ($stmt as $row) {
assertType('array<float|int|string|null>', $row);
}
Expand All @@ -19,45 +18,38 @@ public function defaultFetchType(PDO $pdo, string $q): void
public function specifiedFetchTypes(PDO $pdo, string $q): void
{
$stmt = $pdo->query($q, PDO::FETCH_CLASS);
assertType('PDOStatement<stdClass>', $stmt);
foreach ($stmt as $row) {
assertType('stdClass', $row);
}

$stmt = $pdo->query($q, PDO::FETCH_OBJ);
assertType('PDOStatement<stdClass>', $stmt);
foreach ($stmt as $row) {
assertType('stdClass', $row);
}

$stmt = $pdo->query($q, PDO::FETCH_KEY_PAIR);
assertType('PDOStatement<array{mixed, mixed}>', $stmt);
foreach ($stmt as $row) {
assertType('array{mixed, mixed}', $row);
}

$stmt = $pdo->query($q, PDO::FETCH_ASSOC);
assertType('PDOStatement<array<string, float|int|string|null>>', $stmt);
foreach ($stmt as $row) {
assertType('array<string, float|int|string|null>', $row);
}

$stmt = $pdo->query($q, PDO::FETCH_NUM);
assertType('PDOStatement<array<int<0, max>, float|int|string|null>>', $stmt); // could be list
foreach ($stmt as $row) {
assertType('array<int<0, max>, float|int|string|null>', $row);
assertType('array<int<0, max>, float|int|string|null>', $row); // could be list
}

$stmt = $pdo->query($q, PDO::FETCH_BOTH);
assertType('PDOStatement<array<float|int|string|null>>', $stmt);
foreach ($stmt as $row) {
assertType('array<float|int|string|null>', $row);
}

$stmt = $pdo->query($q, PDO::FETCH_COLUMN);
assertType('PDOStatement', $stmt); // could be PDOStatement<float|int|string|null>
foreach ($stmt as $row) {
assertType('mixed', $row); // could be float|int|string|null
assertType('array<int|string, mixed>', $row); // could be array<int|string, float|int|string|null>
}
}
}
24 changes: 18 additions & 6 deletions tests/default/data/pdo-fetch-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,36 @@ public function supportedFetchTypes(PDO $pdo)
{
// default fetch-type is BOTH
$stmt = $pdo->query('SELECT email, adaid FROM ada');
assertType('PDOStatement<array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}>', $stmt);
foreach ($stmt as $row) {
assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row);
}

$stmt = $pdo->query('SELECT email, adaid FROM ada', PDO::FETCH_NUM);
assertType('PDOStatement<array{string, int<-32768, 32767>}>', $stmt);
foreach ($stmt as $row) {
assertType('array{string, int<-32768, 32767>}', $row);
}

$stmt = $pdo->query('SELECT email, adaid FROM ada', PDO::FETCH_ASSOC);
assertType('PDOStatement<array{email: string, adaid: int<-32768, 32767>}>', $stmt);
foreach ($stmt as $row) {
assertType('array{email: string, adaid: int<-32768, 32767>}', $row);
}

$stmt = $pdo->query('SELECT email, adaid FROM ada', PDO::FETCH_BOTH);
assertType('PDOStatement<array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}>', $stmt);
foreach ($stmt as $row) {
assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row);
}

$stmt = $pdo->query('SELECT email, adaid FROM ada', PDO::FETCH_OBJ);
assertType('PDOStatement<array<int, stdClass>>', $stmt);
foreach ($stmt as $row) {
assertType('array<int, stdClass>', $row);
}
}

public function unsupportedFetchTypes(PDO $pdo)
{
$stmt = $pdo->query('SELECT email, adaid FROM ada', PDO::FETCH_COLUMN);
assertType('PDOStatement', $stmt);
foreach ($stmt as $row) {
assertType('array<int|string, mixed>', $row);
}
}
}
28 changes: 18 additions & 10 deletions tests/default/data/pdo-mysql.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,58 @@ class Foo
public function execute(PDO $pdo)
{
$stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE email <=> :email');
assertType('PDOStatement', $stmt);
$stmt->execute([':email' => null]);
assertType('PDOStatement<array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}>', $stmt);
foreach ($stmt as $row) {
assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row);
}
}

public function aggregateFunctions(PDO $pdo)
{
$query = 'SELECT MAX(adaid), MIN(adaid), COUNT(adaid), AVG(adaid) FROM ada WHERE adaid = 1';
$stmt = $pdo->query($query, PDO::FETCH_ASSOC);
assertType('PDOStatement<array{MAX(adaid): int<-32768, 32767>|null, MIN(adaid): int<-32768, 32767>|null, COUNT(adaid): int, AVG(adaid): numeric-string|null}>', $stmt);
foreach ($stmt as $row) {
assertType('array{MAX(adaid): int<-32768, 32767>|null, MIN(adaid): int<-32768, 32767>|null, COUNT(adaid): int, AVG(adaid): numeric-string|null}', $row);
}
}

public function placeholderInDataPrepared(PDO $pdo)
{
// double quotes within the query
$query = 'SELECT adaid FROM ada WHERE email LIKE ":gesperrt%"';
$stmt = $pdo->prepare($query);
assertType('PDOStatement<array{adaid: int<-32768, 32767>, 0: int<-32768, 32767>}>', $stmt);
$stmt->execute();
assertType('PDOStatement<array{adaid: int<-32768, 32767>, 0: int<-32768, 32767>}>', $stmt);
foreach ($stmt as $row) {
assertType('array{adaid: int<-32768, 32767>, 0: int<-32768, 32767>}', $row);
}

// single quotes within the query
$query = "SELECT adaid FROM ada WHERE email LIKE ':gesperrt%'";
$stmt = $pdo->prepare($query);
assertType('PDOStatement<array{adaid: int<-32768, 32767>, 0: int<-32768, 32767>}>', $stmt);
$stmt->execute();
assertType('PDOStatement<array{adaid: int<-32768, 32767>, 0: int<-32768, 32767>}>', $stmt);
foreach ($stmt as $row) {
assertType('array{adaid: int<-32768, 32767>, 0: int<-32768, 32767>}', $row);
}
}

public function placeholderInDataQuery(PDO $pdo)
{
// double quotes within the query
$query = 'SELECT adaid FROM ada WHERE email LIKE ":gesperrt%"';
$stmt = $pdo->query($query, PDO::FETCH_ASSOC);
assertType('PDOStatement<array{adaid: int<-32768, 32767>}>', $stmt);
foreach ($stmt as $row) {
assertType('array{adaid: int<-32768, 32767>}', $row);
}
}

public function bug541(PDO $pdo)
{
$query = 'SELECT email, adaid FROM ada';
$query .= 'WHERE email <=> :email';
$stmt = $pdo->prepare($query);
assertType('PDOStatement', $stmt);
$stmt->execute([':email' => null]);
assertType('PDOStatement<array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}>', $stmt);
foreach ($stmt as $row) {
assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row);
}
}
}
8 changes: 6 additions & 2 deletions tests/default/data/pdo-pgsql.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ class Foo
public function pgsqlTypes(PDO $pdo)
{
$stmt = $pdo->query('SELECT * FROM typemix', PDO::FETCH_ASSOC);
assertType('PDOStatement<array{pid: int<1, 2147483647>, c_varchar5: string, c_varchar25: string|null, c_varchar255: string, c_date: string|null, c_time: string|null, c_datetime: string|null, c_timestamp: string|null, c_text: string|null, c_enum: mixed, c_bit255: int, c_bit25: int|null, c_bit: int|null, c_int: int<-2147483648, 2147483647>, c_smallint: int<-32768, 32767>, c_bigint: int, c_float: float, c_boolean: bool, c_json: string, c_json_nullable: string|null, c_jsonb: string, c_jsonb_nullable: string|null}>', $stmt);
foreach ($stmt as $row) {
assertType('array{pid: int<1, 2147483647>, c_varchar5: string, c_varchar25: string|null, c_varchar255: string, c_date: string|null, c_time: string|null, c_datetime: string|null, c_timestamp: string|null, c_text: string|null, c_enum: mixed, c_bit255: int, c_bit25: int|null, c_bit: int|null, c_int: int<-2147483648, 2147483647>, c_smallint: int<-32768, 32767>, c_bigint: int, c_float: float, c_boolean: bool, c_json: string, c_json_nullable: string|null, c_jsonb: string, c_jsonb_nullable: string|null}', $row);
}
}

public function aggregateFunctions(PDO $pdo)
{
$query = 'SELECT MAX(adaid), MIN(adaid), COUNT(adaid), AVG(adaid) FROM ada WHERE adaid = 1';
$stmt = $pdo->query($query, PDO::FETCH_ASSOC);
assertType('PDOStatement<array{max: int<-32768, 32767>|null, min: int<-32768, 32767>|null, count: int, avg: float|null}>', $stmt);
foreach ($stmt as $row) {
assertType('array{max: int<-32768, 32767>|null, min: int<-32768, 32767>|null, count: int, avg: float|null}', $row);
}
}
}
Loading

0 comments on commit 436e2e9

Please sign in to comment.