From c98fc6c3ad34d0855306cdad22ea5b900ae4139e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 24 May 2025 18:05:08 +0200 Subject: [PATCH] Introduce PDO::connect dynamicMethodReturnType --- src/Php/PhpVersion.php | 5 ++ .../Php/PDOConnectReturnTypeExtension.php | 78 +++++++++++++++++++ .../Analyser/nsrt/pdo-connect-php84.php | 26 +++++++ 3 files changed, 109 insertions(+) create mode 100644 src/Type/Php/PDOConnectReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/pdo-connect-php84.php diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 5215a30606..434d2b16b0 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -405,4 +405,9 @@ public function supportsBcMathNumberOperatorOverloading(): bool return $this->versionId >= 80400; } + public function hasPDOSubclasses(): bool + { + return $this->versionId >= 80400; + } + } diff --git a/src/Type/Php/PDOConnectReturnTypeExtension.php b/src/Type/Php/PDOConnectReturnTypeExtension.php new file mode 100644 index 0000000000..9aa71ccd27 --- /dev/null +++ b/src/Type/Php/PDOConnectReturnTypeExtension.php @@ -0,0 +1,78 @@ +phpVersion->hasPDOSubclasses() && $methodReflection->getName() === 'connect'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) < 1) { + return null; + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + if (count($constantStrings) === 0) { + return null; + } + + $subclasses = []; + foreach ($constantStrings as $constantString) { + if (str_starts_with($constantString->getValue(), 'mysql:')) { + $subclasses['PDO\Mysql'] = 'PDO\Mysql'; + } elseif (str_starts_with($constantString->getValue(), 'firebird:')) { + $subclasses['PDO\Firebird'] = 'PDO\Firebird'; + } elseif (str_starts_with($constantString->getValue(), 'dblib:')) { + $subclasses['PDO\Dblib'] = 'PDO\Dblib'; + } elseif (str_starts_with($constantString->getValue(), 'odbc:')) { + $subclasses['PDO\Odbc'] = 'PDO\Odbc'; + } elseif (str_starts_with($constantString->getValue(), 'pgsql:')) { + $subclasses['PDO\Pgsql'] = 'PDO\Pgsql'; + } elseif (str_starts_with($constantString->getValue(), 'sqlite:')) { + $subclasses['PDO\Sqlite'] = 'PDO\Sqlite'; + } else { + return null; + } + } + + $returnTypes = []; + foreach ($subclasses as $class) { + $returnTypes[] = new ObjectType($class); + } + + return TypeCombinator::union(...$returnTypes); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/pdo-connect-php84.php b/tests/PHPStan/Analyser/nsrt/pdo-connect-php84.php new file mode 100644 index 0000000000..cd7f6dbd1e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pdo-connect-php84.php @@ -0,0 +1,26 @@ += 8.4 + +namespace PdoConnectPHP84; + +use function PHPStan\Testing\assertType; + +/** + * @param 'mysql:foo'|'pgsql:foo' $mysqlOrPgsql + * @param 'mysql:foo'|'foo:foo' $mysqlOrFoo + */ +function test( + string $string, + string $mysqlOrPgsql, + string $mysqlOrFoo, +) { + assertType('PDO\Mysql', \PDO::connect('mysql:foo')); + assertType('PDO\Firebird', \PDO::connect('firebird:foo')); + assertType('PDO\Dblib', \PDO::connect('dblib:foo')); + assertType('PDO\Odbc', \PDO::connect('odbc:foo')); + assertType('PDO\Pgsql', \PDO::connect('pgsql:foo')); + assertType('PDO\Sqlite', \PDO::connect('sqlite:foo')); + + assertType('PDO', \PDO::connect($string)); + assertType('PDO\Mysql|PDO\Pgsql', \PDO::connect($mysqlOrPgsql)); + assertType('PDO', \PDO::connect($mysqlOrFoo)); +}