From 82b43a7288ac72be1314f40851a6d086bc19cc43 Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Wed, 8 Apr 2026 08:41:23 -0400 Subject: [PATCH 1/7] 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 4ad74a7c..f95525a8 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 530f73d59d350b18f48e71178ae639ac4f25beeb Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Wed, 8 Apr 2026 08:43:42 -0400 Subject: [PATCH 2/7] 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 1e48439f..7e4a61f4 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 420a2b166de6de13b84edf3a3bc7670b0fd0b74a Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Mon, 13 Apr 2026 15:04:12 -0400 Subject: [PATCH 3/7] 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 7e4a61f4..e00d940d 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 fc0205b9893de7acf06bf2d05db5abd9bbc66dd4 Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Mon, 13 Apr 2026 15:09:30 -0400 Subject: [PATCH 4/7] 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 e00d940d..6d8ddc39 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); } } From ed1f09f8cab7047709fd4043504a01f6a0c8bf43 Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Fri, 17 Apr 2026 09:54:02 -0400 Subject: [PATCH 5/7] Apply suggestion from @staabm Co-authored-by: Markus Staab --- tests/default/data/pdo-stmt-execute.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/default/data/pdo-stmt-execute.php b/tests/default/data/pdo-stmt-execute.php index f95525a8..060b36a0 100644 --- a/tests/default/data/pdo-stmt-execute.php +++ b/tests/default/data/pdo-stmt-execute.php @@ -34,7 +34,7 @@ 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); From 3da9ce4bc55cbe5513283bf6fd8d2dcafbe28cc1 Mon Sep 17 00:00:00 2001 From: "Etienne V. Labelle" Date: Fri, 17 Apr 2026 10:39:18 -0400 Subject: [PATCH 6/7] Revert to instanceof ConstantStringType and baseline the deprecation --- phpstan-baseline.neon | 6 ++++++ .../PdoStatementExecuteTypeSpecifyingExtension.php | 10 +++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e07441b1..2515d7ca 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -72,6 +72,12 @@ parameters: count: 2 path: src/QueryReflection/QuerySimulation.php + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php + - message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' identifier: phpstanApi.instanceofType diff --git a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php index 6d8ddc39..310b275b 100644 --- a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php +++ b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php @@ -14,6 +14,7 @@ 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; @@ -64,7 +65,7 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa { $args = $methodCall->getArgs(); - + $stmtReflection = new PdoStatementReflection(); $queryExpr = $stmtReflection->findPrepareQueryStringExpression($methodCall); @@ -73,7 +74,6 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa } $queryReflection = new QueryReflection(); - if (0 === \count($args)) { $parameterKeys = []; @@ -83,13 +83,9 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa $bindArgs = $bindCall->getArgs(); if (\count($bindArgs) >= 2) { $keyType = $scope->getType($bindArgs[0]->value); - $constantStrings = $keyType->getConstantStrings(); - if ($keyType instanceof ConstantIntegerType) { + if ($keyType instanceof ConstantIntegerType || $keyType instanceof ConstantStringType) { $parameterKeys[] = $keyType; $parameterValues[] = $scope->getType($bindArgs[1]->value); - } elseif ([] !== $constantStrings) { - $parameterKeys[] = $constantStrings[0]; - $parameterValues[] = $scope->getType($bindArgs[1]->value); } } } From cb9a7e5d4780ba0570bd3e384ebf8d74a13ab142 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 17 Apr 2026 19:07:42 +0200 Subject: [PATCH 7/7] record --- .../config/.phpunit-phpstan-dba-mysqli.cache | 28 +++++++++++++------ .../.phpunit-phpstan-dba-pdo-mysql.cache | 28 +++++++++++++------ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/tests/default/config/.phpunit-phpstan-dba-mysqli.cache b/tests/default/config/.phpunit-phpstan-dba-mysqli.cache index e884f353..6e4bc6d0 100644 --- a/tests/default/config/.phpunit-phpstan-dba-mysqli.cache +++ b/tests/default/config/.phpunit-phpstan-dba-mysqli.cache @@ -310,10 +310,6 @@ FROM ada' => array ( 'type-description' => 'array{email: string, 0: string}', ), - 3 => - array ( - 'type-description' => 'array{email: string}', - ), ), ), 'SELECT email adaid WHERE gesperrt freigabe1u1 FROM ada' => @@ -332,10 +328,6 @@ FROM ada' => array ( 'type-description' => 'array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', ), - 3 => - array ( - 'type-description' => 'array{email: string, adaid: int<-32768, 32767>}', - ), ), ), 'SELECT email, adaid FROM ada LIMIT 1' => @@ -378,6 +370,16 @@ FROM ada' => ), ), ), + 'SELECT email, adaid FROM ada WHERE adaid = \'email@example.org\' and email = \'1\'' => + array ( + 'result' => + array ( + 5 => + array ( + 'type-description' => 'array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', + ), + ), + ), 'SELECT email, adaid FROM ada WHERE adaid = 1' => array ( 'result' => @@ -492,6 +494,16 @@ FROM ada' => ), ), ), + 'SELECT email, adaid FROM ada WHERE email = NULL AND email = \'1001\'' => + array ( + 'result' => + array ( + 5 => + array ( + 'type-description' => 'array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', + ), + ), + ), 'SELECT email, adaid FROM ada WHERE email=\'foo\'' => array ( 'result' => diff --git a/tests/default/config/.phpunit-phpstan-dba-pdo-mysql.cache b/tests/default/config/.phpunit-phpstan-dba-pdo-mysql.cache index 9541a51e..8e5caf13 100644 --- a/tests/default/config/.phpunit-phpstan-dba-pdo-mysql.cache +++ b/tests/default/config/.phpunit-phpstan-dba-pdo-mysql.cache @@ -310,10 +310,6 @@ FROM ada' => array ( 'type-description' => 'array{email: string, 0: string}', ), - 3 => - array ( - 'type-description' => 'array{email: string}', - ), ), ), 'SELECT email adaid WHERE gesperrt freigabe1u1 FROM ada' => @@ -332,10 +328,6 @@ FROM ada' => array ( 'type-description' => 'array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', ), - 3 => - array ( - 'type-description' => 'array{email: string, adaid: int<-32768, 32767>}', - ), ), ), 'SELECT email, adaid FROM ada LIMIT 1' => @@ -378,6 +370,16 @@ FROM ada' => ), ), ), + 'SELECT email, adaid FROM ada WHERE adaid = \'email@example.org\' and email = \'1\'' => + array ( + 'result' => + array ( + 5 => + array ( + 'type-description' => 'array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', + ), + ), + ), 'SELECT email, adaid FROM ada WHERE adaid = 1' => array ( 'result' => @@ -492,6 +494,16 @@ FROM ada' => ), ), ), + 'SELECT email, adaid FROM ada WHERE email = NULL AND email = \'1001\'' => + array ( + 'result' => + array ( + 5 => + array ( + 'type-description' => 'array{email: string, 0: string, adaid: int<-32768, 32767>, 1: int<-32768, 32767>}', + ), + ), + ), 'SELECT email, adaid FROM ada WHERE email=\'foo\'' => array ( 'result' =>