From 67df2d06338a512a544ac81a05dfaad356dc52cb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 30 Oct 2026 12:14:54 +0100 Subject: [PATCH 1/6] Support mysqli_result->fetch_assoc --- tests/default/data/mysqli.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/default/data/mysqli.php b/tests/default/data/mysqli.php index 5e9123e6d..13bdfe11c 100644 --- a/tests/default/data/mysqli.php +++ b/tests/default/data/mysqli.php @@ -57,4 +57,11 @@ public function unionResult(mysqli $mysqli) } } } + + public function fetchAssoc(mysqli $mysqli, string $query) + { + $result = mysqli_query($mysqli, 'SELECT adaid FROM ada'); + assertType('array{adaid: int<-32768, 32767>}', $result->fetch_assoc()); + } + } From 81e7b9f6c5e7539abbffb884610f27f4d3177628 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 30 Oct 2026 14:01:45 +0100 Subject: [PATCH 2/6] MysqliResultDynamicReturnTypeExtension --- config/extensions.neon | 5 +++ ...MysqliResultDynamicReturnTypeExtension.php | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/Extensions/MysqliResultDynamicReturnTypeExtension.php diff --git a/config/extensions.neon b/config/extensions.neon index 437374315..386a83fc8 100644 --- a/config/extensions.neon +++ b/config/extensions.neon @@ -69,6 +69,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: staabm\PHPStanDba\Extensions\MysqliResultDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - class: staabm\PHPStanDba\Extensions\MysqliQueryDynamicReturnTypeExtension tags: diff --git a/src/Extensions/MysqliResultDynamicReturnTypeExtension.php b/src/Extensions/MysqliResultDynamicReturnTypeExtension.php new file mode 100644 index 000000000..9e17f7b72 --- /dev/null +++ b/src/Extensions/MysqliResultDynamicReturnTypeExtension.php @@ -0,0 +1,38 @@ +getName(); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $resultType = $scope->getType($methodCall->var); + if ( + count($methodCall->getArgs()) === 0 + && $resultType instanceof MysqliResultObjectType + ) { + return $resultType->getIterableValueType(); + } + return null; + } +} From 02f755832e1c35477759e83d124dfdbe5f98b207 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 31 Oct 2026 07:20:46 +0100 Subject: [PATCH 3/6] add more expectations --- tests/default/data/mysqli.php | 44 ++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/default/data/mysqli.php b/tests/default/data/mysqli.php index 13bdfe11c..732df1329 100644 --- a/tests/default/data/mysqli.php +++ b/tests/default/data/mysqli.php @@ -58,10 +58,52 @@ public function unionResult(mysqli $mysqli) } } - public function fetchAssoc(mysqli $mysqli, string $query) + public function mysqliResultfetch(mysqli $mysqli) { $result = mysqli_query($mysqli, 'SELECT adaid FROM ada'); assertType('array{adaid: int<-32768, 32767>}', $result->fetch_assoc()); + + $result = mysqli_query($mysqli, 'SELECT adaid FROM ada'); + assertType('list}>', $result->fetch_all()); + assertType('list}>', $result->fetch_all(MYSQLI_ASSOC)); + assertType('list}>', $result->fetch_all(MYSQLI_NUM)); + assertType('list, 0: int<-32768, 32767>}>', $result->fetch_all(MYSQLI_BOTH)); + } + + public function fetchAllColumns(mysqli $mysqli) + { + $result = $mysqli->query('SELECT email, adaid FROM ada'); + assertType('list}>', $result->fetch_all()); + assertType('list}>', $result->fetch_all(MYSQLI_ASSOC)); + assertType('list}>', $result->fetch_all(MYSQLI_NUM)); + assertType('list, 1: int<-32768, 32767>}>', $result->fetch_all(MYSQLI_BOTH)); + assertType('array{email: string, adaid: int<-32768, 32767>}', $result->fetch_assoc()); + foreach ($result as $row) { + assertType('array{email: string, adaid: int<-32768, 32767>}', $row); + } + } + + public function fetchAllUnionResult(mysqli $mysqli) + { + $queries = ['SELECT adaid FROM ada', 'SELECT email FROM ada']; + + foreach ($queries as $query) { + $result = $mysqli->query($query); + assertType('list}|array{string}>', $result->fetch_all()); + assertType('list}|array{email: string}>', $result->fetch_all(MYSQLI_ASSOC)); + assertType('list}|array{string}>', $result->fetch_all(MYSQLI_NUM)); + assertType('list, 0: int<-32768, 32767>}|array{email: string, 0: string}>', $result->fetch_all(MYSQLI_BOTH)); + assertType('array{adaid: int<-32768, 32767>}|array{email: string}', $result->fetch_assoc()); + } + } + + public function fetchAllUnionMode(mysqli $mysqli) + { + $result = $mysqli->query('SELECT adaid FROM ada'); + $mode = rand(0, 1) ? MYSQLI_ASSOC : MYSQLI_NUM; + assertType('list}|array{int<-32768, 32767>}>', $result->fetch_all($mode)); + assertType('list, 0: int<-32768, 32767>}>', $result->fetch_all(MYSQLI_ASSOC | MYSQLI_NUM)); + assertType('list}>', $result->FETCH_ALL(mode: MYSQLI_ASSOC)); } } From 2dce25fa3ce8403e48309811157eb5a6fd2e76f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Oct 2026 06:32:49 +0000 Subject: [PATCH 4/6] Handle mysqli result row types Co-authored-by: staabm <120441+staabm@users.noreply.github.com> --- .../MysqliQueryDynamicReturnTypeExtension.php | 5 +-- ...MysqliResultDynamicReturnTypeExtension.php | 32 ++++++++++++++++--- .../MysqliResultObjectType.php | 20 ++++++++---- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/Extensions/MysqliQueryDynamicReturnTypeExtension.php b/src/Extensions/MysqliQueryDynamicReturnTypeExtension.php index 21b315316..6523f7e83 100644 --- a/src/Extensions/MysqliQueryDynamicReturnTypeExtension.php +++ b/src/Extensions/MysqliQueryDynamicReturnTypeExtension.php @@ -130,10 +130,7 @@ private function inferResultType(Expr $queryExpr, Scope $scope): ?Type return null; } - $resultObjectType = new MysqliResultObjectType(\mysqli_result::class); - $resultObjectType->setRowType($resultType); - - $objects[] = $resultObjectType; + $objects[] = MysqliResultObjectType::newWithRowType($resultType); } if (0 === \count($objects)) { diff --git a/src/Extensions/MysqliResultDynamicReturnTypeExtension.php b/src/Extensions/MysqliResultDynamicReturnTypeExtension.php index 9e17f7b72..3513afc3b 100644 --- a/src/Extensions/MysqliResultDynamicReturnTypeExtension.php +++ b/src/Extensions/MysqliResultDynamicReturnTypeExtension.php @@ -10,6 +10,8 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use staabm\PHPStanDba\MysqliReflection\MysqliResultObjectType; final class MysqliResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension @@ -27,12 +29,32 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { $resultType = $scope->getType($methodCall->var); - if ( - count($methodCall->getArgs()) === 0 - && $resultType instanceof MysqliResultObjectType - ) { - return $resultType->getIterableValueType(); + if (count($methodCall->getArgs()) === 0) { + return $this->getRowType($resultType); } return null; } + + private function getRowType(Type $resultType): ?Type + { + if ($resultType instanceof MysqliResultObjectType) { + return $resultType->getRowType(); + } + + if (! $resultType instanceof UnionType) { + return null; + } + + $rowTypes = []; + foreach ($resultType->getTypes() as $type) { + $rowType = $this->getRowType($type); + if ($rowType === null) { + return null; + } + + $rowTypes[] = $rowType; + } + + return TypeCombinator::union(...$rowTypes); + } } diff --git a/src/MysqliReflection/MysqliResultObjectType.php b/src/MysqliReflection/MysqliResultObjectType.php index d1d1f6ad9..707b692eb 100644 --- a/src/MysqliReflection/MysqliResultObjectType.php +++ b/src/MysqliReflection/MysqliResultObjectType.php @@ -4,6 +4,7 @@ namespace staabm\PHPStanDba\MysqliReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -12,18 +13,25 @@ final class MysqliResultObjectType extends ObjectType { private ?Type $rowType = null; - public function setRowType(Type $rowType): void + public static function newWithRowType(Type $rowType): self { - $this->rowType = $rowType; + $new = new self(\mysqli_result::class); + $new->rowType = $rowType; + return $new; } - public function getIterableValueType(): \PHPStan\Type\Type + public function getRowType(): Type { - if ($this->rowType !== null) { - return $this->rowType; + if ($this->rowType === null) { + throw new ShouldNotHappenException(); } - return parent::getIterableValueType(); + return $this->rowType; + } + + public function getIterableValueType(): Type + { + return $this->getRowType(); } // differentiate objects based on the local properties, From 4e98824642b235afa3c5b65466c3975a0bc7598d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Oct 2026 06:53:53 +0000 Subject: [PATCH 5/6] Implement mysqli fetch_all inference Co-authored-by: staabm <120441+staabm@users.noreply.github.com> --- ...MysqliResultDynamicReturnTypeExtension.php | 112 +++++++++++++++++- tests/default/data/mysqli.php | 2 +- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/src/Extensions/MysqliResultDynamicReturnTypeExtension.php b/src/Extensions/MysqliResultDynamicReturnTypeExtension.php index 3513afc3b..37fe09244 100644 --- a/src/Extensions/MysqliResultDynamicReturnTypeExtension.php +++ b/src/Extensions/MysqliResultDynamicReturnTypeExtension.php @@ -8,7 +8,13 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicMethodReturnTypeExtension; +use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; @@ -23,16 +29,48 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return 'fetch_assoc' === $methodReflection->getName(); + return \in_array(strtolower($methodReflection->getName()), ['fetch_assoc', 'fetch_all'], true); } public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { $resultType = $scope->getType($methodCall->var); - if (count($methodCall->getArgs()) === 0) { - return $this->getRowType($resultType); + $methodName = strtolower($methodReflection->getName()); + if ('fetch_assoc' === $methodName) { + if (count($methodCall->getArgs()) === 0) { + return $this->getRowType($resultType); + } + + return null; + } + + $modes = [\MYSQLI_NUM]; + if (count($methodCall->getArgs()) > 0) { + $modes = $this->getModes($scope->getType($methodCall->getArgs()[0]->value)); + if ($modes === null) { + return null; + } + } + + $rowType = $this->getRowType($resultType); + if ($rowType === null) { + return null; + } + + $rows = []; + foreach ($modes as $mode) { + $modeRowType = $this->getRowTypeForMode($rowType, $mode); + if ($modeRowType === null) { + return null; + } + + $rows[] = $modeRowType; } - return null; + + return TypeCombinator::intersect( + new ArrayType(new IntegerType(), TypeCombinator::union(...$rows)), + new AccessoryArrayListType() + ); } private function getRowType(Type $resultType): ?Type @@ -57,4 +95,70 @@ private function getRowType(Type $resultType): ?Type return TypeCombinator::union(...$rowTypes); } + + /** + * @return list|null + */ + private function getModes(Type $modeType): ?array + { + if ($modeType instanceof ConstantIntegerType) { + return [$modeType->getValue()]; + } + + if (! $modeType instanceof UnionType) { + return null; + } + + $modes = []; + foreach ($modeType->getTypes() as $type) { + $typeModes = $this->getModes($type); + if ($typeModes === null) { + return null; + } + + foreach ($typeModes as $mode) { + $modes[] = $mode; + } + } + + return $modes; + } + + private function getRowTypeForMode(Type $rowType, int $mode): ?Type + { + if (! \in_array($mode, [\MYSQLI_NUM, \MYSQLI_ASSOC, \MYSQLI_BOTH], true)) { + return null; + } + + $rowTypes = []; + foreach ($rowType->getConstantArrays() as $arrayType) { + $rowTypes[] = $this->convertRowType($arrayType, $mode); + } + + if (count($rowTypes) === 0) { + return null; + } + + return TypeCombinator::union(...$rowTypes); + } + + private function convertRowType(ConstantArrayType $rowType, int $mode): ConstantArrayType + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + $numericIndex = 0; + + foreach ($rowType->getKeyTypes() as $index => $keyType) { + $valueType = $rowType->getValueTypes()[$index]; + if ($mode === \MYSQLI_ASSOC || $mode === \MYSQLI_BOTH) { + $builder->setOffsetValueType($keyType, $valueType); + } + + if ($mode === \MYSQLI_NUM || $mode === \MYSQLI_BOTH) { + $builder->setOffsetValueType(new ConstantIntegerType($numericIndex), $valueType); + $numericIndex++; + } + } + + return $builder->getArray(); + } } diff --git a/tests/default/data/mysqli.php b/tests/default/data/mysqli.php index 732df1329..10d8889c9 100644 --- a/tests/default/data/mysqli.php +++ b/tests/default/data/mysqli.php @@ -103,7 +103,7 @@ public function fetchAllUnionMode(mysqli $mysqli) $mode = rand(0, 1) ? MYSQLI_ASSOC : MYSQLI_NUM; assertType('list}|array{int<-32768, 32767>}>', $result->fetch_all($mode)); assertType('list, 0: int<-32768, 32767>}>', $result->fetch_all(MYSQLI_ASSOC | MYSQLI_NUM)); - assertType('list}>', $result->FETCH_ALL(mode: MYSQLI_ASSOC)); + assertType('list}>', $result->fetch_all(MYSQLI_ASSOC)); } } From 398a7c42c7fdb27ef322a5f88364b479e57c1efd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Oct 2026 07:04:13 +0000 Subject: [PATCH 6/6] Fix mysqli row conversion PHPStan return type Co-authored-by: staabm <120441+staabm@users.noreply.github.com> --- src/Extensions/MysqliResultDynamicReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extensions/MysqliResultDynamicReturnTypeExtension.php b/src/Extensions/MysqliResultDynamicReturnTypeExtension.php index 37fe09244..a1ed6bd8f 100644 --- a/src/Extensions/MysqliResultDynamicReturnTypeExtension.php +++ b/src/Extensions/MysqliResultDynamicReturnTypeExtension.php @@ -142,7 +142,7 @@ private function getRowTypeForMode(Type $rowType, int $mode): ?Type return TypeCombinator::union(...$rowTypes); } - private function convertRowType(ConstantArrayType $rowType, int $mode): ConstantArrayType + private function convertRowType(ConstantArrayType $rowType, int $mode): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); $numericIndex = 0;