From bac4188de53ae216a9d857be144000b0b002864a Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Wed, 8 Apr 2026 08:41:23 -0400 Subject: [PATCH 1/4] test: add regression case for PDOStatement::execute returning mixed rows --- tests/default/data/pdo-stmt-execute.php | 41 ++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/default/data/pdo-stmt-execute.php b/tests/default/data/pdo-stmt-execute.php index 4ad74a7ca..f95525a8f 100644 --- a/tests/default/data/pdo-stmt-execute.php +++ b/tests/default/data/pdo-stmt-execute.php @@ -9,6 +9,7 @@ class Foo { public function execute(PDO $pdo) { + $stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE adaid = :adaid'); $stmt->execute([':adaid' => 1]); foreach ($stmt as $row) { @@ -33,11 +34,12 @@ public function execute(PDO $pdo) assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row); } - $stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE adaid = ? and email = ?'); + $stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE adaid = ? and email = ? '); $stmt->execute([1, 'email@example.org']); foreach ($stmt as $row) { assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row); } + } public function executeWithBindCalls(PDO $pdo) @@ -48,6 +50,43 @@ public function executeWithBindCalls(PDO $pdo) $stmt->bindParam(':test1', $test); $stmt->bindValue(':test2', 1001); $stmt->execute(); + + $stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE adaid = :adaid'); + $stmt->bindValue(':adaid', 1); + $stmt->execute(); + foreach ($stmt as $row) { + assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row); + } + + $stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE adaid = :adaid'); + $stmt->bindValue('adaid', 1); + $stmt->execute(); // prefixed ":" is optional + foreach ($stmt as $row) { + assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row); + } + + $stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE email = :email'); + $stmt->bindValue(':email', 'email@example.org'); + $stmt->execute(); + foreach ($stmt as $row) { + assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row); + } + + $stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE adaid = ?'); + $stmt->bindValue(1, 1); + $stmt->execute(); + foreach ($stmt as $row) { + assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row); + } + + $stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE adaid = ? and email = ? '); + $stmt->bindValue(1, 1); + $stmt->bindValue(2, 'email@example.org'); + $stmt->execute(); + foreach ($stmt as $row) { + assertType('array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', $row); + } + } public function errors(PDO $pdo) From a7649f6f659449e39ff9a015752e8f12504f1545 Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Wed, 8 Apr 2026 08:43:42 -0400 Subject: [PATCH 2/4] fix: preserve typed rows for PDO statements using bindValue() and execute() --- ...tatementExecuteTypeSpecifyingExtension.php | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php index 1e48439fd..7e4a61f41 100644 --- a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php +++ b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php @@ -12,6 +12,9 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\MethodTypeSpecifyingExtension; use PHPStan\Type\Type; use staabm\PHPStanDba\PdoReflection\PdoStatementReflection; @@ -62,9 +65,7 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa { $args = $methodCall->getArgs(); - if (0 === \count($args)) { - return null; - } + $stmtReflection = new PdoStatementReflection(); $queryExpr = $stmtReflection->findPrepareQueryStringExpression($methodCall); @@ -73,7 +74,28 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa } $queryReflection = new QueryReflection(); - $parameterTypes = $queryReflection->resolveParameterTypes($args[0]->value, $scope); + + + if (0 === \count($args)) { + $parameterKeys = []; + $parameterValues = []; + + foreach ($stmtReflection->findPrepareBindCalls($methodCall) as $bindCall) { + $bindArgs = $bindCall->getArgs(); + if (\count($bindArgs) >= 2) { + $keyType = $scope->getType($bindArgs[0]->value); + if ($keyType instanceof ConstantIntegerType || $keyType instanceof ConstantStringType) { + $parameterKeys[] = $keyType; + $parameterValues[] = $scope->getType($bindArgs[1]->value); + } + } + } + + $parameterTypes = new ConstantArrayType($parameterKeys, $parameterValues); + } else { + $parameterTypes = $queryReflection->resolveParameterTypes($args[0]->value, $scope); + } + $queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, $scope); $reflectionFetchType = QueryReflection::getRuntimeConfiguration()->getDefaultFetchMode(); From 43a371a2afb1cb571fd8e0c4a74458b5c1ae8943 Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Mon, 13 Apr 2026 15:04:12 -0400 Subject: [PATCH 3/4] Replace instanceof ConstantStringType with getConstantStrings() API --- .../PdoStatementExecuteTypeSpecifyingExtension.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php index 7e4a61f41..e00d940db 100644 --- a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php +++ b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php @@ -14,7 +14,6 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\MethodTypeSpecifyingExtension; use PHPStan\Type\Type; use staabm\PHPStanDba\PdoReflection\PdoStatementReflection; @@ -84,8 +83,9 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa $bindArgs = $bindCall->getArgs(); if (\count($bindArgs) >= 2) { $keyType = $scope->getType($bindArgs[0]->value); - if ($keyType instanceof ConstantIntegerType || $keyType instanceof ConstantStringType) { - $parameterKeys[] = $keyType; + $constantStrings = $keyType->getConstantStrings(); + if ($keyType instanceof ConstantIntegerType || [] !== $constantStrings) { + $parameterKeys[] = [] !== $constantStrings ? $constantStrings[0] : $keyType; $parameterValues[] = $scope->getType($bindArgs[1]->value); } } From dc55e81436c1973b377a867bbf0480e386f412ad Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Mon, 13 Apr 2026 15:09:30 -0400 Subject: [PATCH 4/4] Split ConstantIntegerType/ConstantStringType branches to satisfy ConstantArrayType constructor type --- .../PdoStatementExecuteTypeSpecifyingExtension.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php index e00d940db..6d8ddc39e 100644 --- a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php +++ b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php @@ -84,8 +84,11 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa if (\count($bindArgs) >= 2) { $keyType = $scope->getType($bindArgs[0]->value); $constantStrings = $keyType->getConstantStrings(); - if ($keyType instanceof ConstantIntegerType || [] !== $constantStrings) { - $parameterKeys[] = [] !== $constantStrings ? $constantStrings[0] : $keyType; + if ($keyType instanceof ConstantIntegerType) { + $parameterKeys[] = $keyType; + $parameterValues[] = $scope->getType($bindArgs[1]->value); + } elseif ([] !== $constantStrings) { + $parameterKeys[] = $constantStrings[0]; $parameterValues[] = $scope->getType($bindArgs[1]->value); } }