Skip to content

Add multi-variant overloads for PDOStatement::fetchAll() in function signature map#5630

Open
phpstan-bot wants to merge 7 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-8ctq654
Open

Add multi-variant overloads for PDOStatement::fetchAll() in function signature map#5630
phpstan-bot wants to merge 7 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-8ctq654

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PDOStatement::fetchAll() had a single function map entry with fetch_argument typed as int|string|callable, which caused false positives when passing constructor arguments (an array) as the third parameter in FETCH_CLASS mode. The fix adds proper multi-variant overloads.

Changes

  • resources/functionMap.php: Replaced single PDOStatement::fetchAll entry with 4 variants:
    • Base: (int $how) — for simple fetch modes like FETCH_ASSOC
    • Variant 1: (int $fetch_column, int $colno) — for FETCH_COLUMN
    • Variant 2: (int $fetch_class, string $classname, ?array $ctorargs) — for FETCH_CLASS
    • Variant 3: (int $fetch_func, callable $callback) — for FETCH_FUNC
  • resources/functionMap_php80delta.php: Same overloads with array return type (instead of array|false)
  • Added regression test in tests/PHPStan/Rules/Methods/data/bug-5509.php

Root cause

The function map had a single signature where the second parameter was typed as int|string|callable — a union of all possible types across different fetch modes. When calling fetchAll(PDO::FETCH_CLASS, StdClass::class, [new StdClass]), the array constructor argument didn't match any type in the variadic expansion of int|string|callable. The fix follows the established pattern already used by PDO::query and PDOStatement::setFetchMode, which have separate overload variants for each fetch mode.

Analogous cases checked

  • PDO::query — already has proper overloads (variants '1, '2, '3)
  • PDOStatement::setFetchMode — already has proper overloads (variants '1, '2, '3)
  • PDOStatement::fetch — has fixed parameter types (int, int, int), no mode-dependent types needed

Test

  • tests/PHPStan/Rules/Methods/data/bug-5509.php: Tests all valid calling patterns of fetchAll()FETCH_CLASS with class name and constructor args, FETCH_CLASS with just class name, FETCH_COLUMN with column number, FETCH_FUNC with callable, no args, and just mode.

Fixes phpstan/phpstan#5509

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add PHP8+ test with

$pdo = new PDO(
	dsn: "mysql:host=servername;dbname=dbname;charset=utf8mb4",
	username: "username",
	password: "password"
);

$dbQuery = $pdo->prepare("SELECT * FROM table;");

$dbQuery->fetchAll(mode: PDO::FETCH_DEFAULT);

$dbQuery->fetchAll(
	mode: PDO::FETCH_COLUMN,
	column: 1
);

$dbQuery->fetchAll(
	mode: PDO::FETCH_CLASS,
	class: "class"
);

Name arguments should work correctly

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. I've added named argument tests for PDOStatement::fetchAll() using mode: which is the only valid named argument for this method.

Regarding the reviewer's suggestion to also test column: and class: as named arguments — those cannot work because PDOStatement::fetchAll is an internal PHP function whose signature is fetchAll(int $mode = PDO::FETCH_DEFAULT, mixed ...$args). PHP does not accept arbitrary named parameters for variadic params on internal functions (it throws "does not accept unknown named parameters" at runtime). PHPStan correctly reports "Unknown parameter" for these cases.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchAll() uses overloaded signatures — the available named parameters change depending on which $mode you pass.

So the four signature

PDOStatement::fetchAll(int $mode = PDO::FETCH_DEFAULT): array
PDOStatement::fetchAll(int $mode = PDO::FETCH_COLUMN, int $column): array
PDOStatement::fetchAll(int $mode = PDO::FETCH_CLASS, string $class, ?array $constructorArgs): array
PDOStatement::fetchAll(int $mode = PDO::FETCH_FUNC, callable $callback): array
```
should be usable with the named parameters (for all parameters).

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Here's a summary of what was done to address the review:

Problem: The reviewer requested that PDOStatement::fetchAll() support named arguments for all parameters (not just mode:), matching PHP 8.4's actual runtime behavior where column:, class:, and constructorArgs: are accepted as named arguments.

Changes made:

  1. Function map parameter names (functionMap.php, functionMap_php80delta.php): Renamed parameters to match PHP's actual named argument names: mode, column, class, constructorArgs, callback (previously how, fetch_column/colno, fetch_class/classname/ctorargs, fetch_func).

  2. Php8SignatureMapProvider: Two changes:

    • Modified the hasMiddleVariadicParam check to only consider function map variadic params (not native), preventing incorrect skipping of variants with more params than native.
    • When a native variadic param is encountered, conditionally include remaining function map params in the named variant (using their function map names), but only when all preceding param names match native. This prevents false positives for methods like PDO::query that don't support variadic-slot named params.
  3. ParametersAcceptorSelector: Enhanced type matching to look up argument types by parameter name when numeric indices don't match (for named argument calls), and to reject variants whose required params don't have matching named arguments.

  4. Test: Added named argument test cases for column:, class:, and constructorArgs:.

Comment thread resources/functionMap.php Outdated
Comment on lines +7836 to +7839
'PDOStatement::fetchAll' => ['array|false', 'mode='=>'int'],
'PDOStatement::fetchAll\'1' => ['array|false', 'mode'=>'int', 'column'=>'int'],
'PDOStatement::fetchAll\'2' => ['array|false', 'mode'=>'int', 'class'=>'string', 'constructorArgs='=>'?array'],
'PDOStatement::fetchAll\'3' => ['array|false', 'mode'=>'int', 'callback'=>'callable'],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not rename in functionMap since it's only for PHP 7.4 or less

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Reverted functionMap.php to keep the original PHP 7.4 parameter names (how, fetch_column/colno, fetch_class/classname/ctorargs, fetch_func/callback) since named arguments don't exist before PHP 8.0. The PHP 8 names (mode, column, class, constructorArgs, callback) remain only in functionMap_php80delta.php where they're needed for named argument support. Tests and static analysis pass.

}

$typesCount = count($types);
$hasNamedTypes = count($types) > 0 && !array_is_list($types);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use typesCount var here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I addressed both review comments:

  1. functionMap.php: Reverted parameter names back to the original PHP 7.4 names (how, fetch_column, colno, fetch_class, classname, ctorargs, fetch_func) since this file is only for PHP 7.4 or less where named arguments don't exist. The PHP 8.0 delta file retains the correct modern names (mode, column, class, constructorArgs, callback).

  2. ParametersAcceptorSelector.php: Used the existing $typesCount variable instead of calling count($types) again.

All tests pass and make phpstan reports no errors.

@@ -650,11 +651,18 @@ public static function selectFromTypes(
$acceptableAcceptor = GenericParametersAcceptorResolver::resolve($types, $acceptableAcceptor);
foreach ($acceptableAcceptor->getParameters() as $i => $parameter) {
if (!isset($types[$i])) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This nested if could be simplified by inversing the condition

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed. The review comment asked to simplify the nested if by inverting the condition — I flattened the if (!isset($types[$i])) { ... } else { $type = $types[$i]; } into a single if/elseif chain with isset($types[$i]) as the first branch, removing one level of nesting. Tests and static analysis both pass.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also/instead improve ParametersAcceptorSelector::selectFromArgs in order to support this method ? Like it's the case for
ArrayMapArgVisitor
CurlSetOptArgVisitor
ArrayFilterArgVisitor
ImplodeArgVisitor
and more ?

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Both pass cleanly. There are no code changes needed for this review comment — it's a design question that should be answered in the PR discussion.

The reviewer's question "Should we also/instead improve ParametersAcceptorSelector::selectFromArgs?" has a clear answer: No, the multi-variant overload approach is the correct pattern here. Here's why:

  1. Consistency: PDO::query and PDOStatement::setFetchMode both use the same multi-variant overload approach without any argument visitor. This PR follows the established pattern.

  2. Different use case: The selectFromArgs visitors (like CurlSetOptArgVisitor) solve a different problem — they refine a mixed parameter type based on the value of another parameter in a single function signature. For fetchAll(), the variants already have distinct parameter types (int vs string vs callable for the 2nd param), so selectFromTypes can already pick the correct variant without inspecting constant values.

  3. Marginal benefit: A visitor that inspects the actual PDO::FETCH_* constant value would only add value if we wanted to reject calls where e.g. PDO::FETCH_ASSOC is passed with a column number. But that's a stricter check than needed, and would cause false positives when the mode isn't statically known (e.g., comes from a variable or function call).

Since there are no code changes to make and nothing to push, the response to this review is explanatory. The tests and static analysis all pass.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FILE: src/Reflection/SignatureMap/Php8SignatureMapProvider.php

FOUND 1 ERROR AFFECTING 1 LINE

298 | ERROR | [x] Function array_slice() should not be referenced via a
| | fallback global name, but via a use statement.
| | (SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFallbackGlobalName)

PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Fixed the coding standard violation by adding use function array_slice; to the import list in Php8SignatureMapProvider.php. Both make phpstan and the tests pass. Committed and pushed.

VincentLanglet and others added 7 commits May 10, 2026 20:58
…n signature map

- Replace the single `PDOStatement::fetchAll` signature (which combined all
  parameter types into one union) with proper multi-variant overloads matching
  `PDO::query` and `PDOStatement::setFetchMode` patterns
- Add variant '1 for FETCH_COLUMN mode (int $mode, int $colno)
- Add variant '2 for FETCH_CLASS mode (int $mode, string $classname, ?array $ctorargs)
- Add variant '3 for FETCH_FUNC mode (int $mode, callable $callback)
- Update both base functionMap.php and functionMap_php80delta.php
- Verified PDO::query and PDOStatement::setFetchMode already had correct overloads
The `mode:` named argument works correctly with the multi-variant
overloads. Note that `column:` and `class:` cannot be used as named
arguments because PDOStatement::fetchAll is an internal function with
variadic `...$args` — PHP does not accept arbitrary named parameters
for variadic params on internal functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eters

PHP 8.4 accepts named arguments like `column:`, `class:`, and
`constructorArgs:` for PDOStatement::fetchAll() despite its native
signature using `mixed ...$args`. This commit:

1. Renames function map parameters to match PHP's actual named params
   (`mode`, `column`, `class`, `constructorArgs`, `callback`)

2. Updates Php8SignatureMapProvider to include function map parameters
   past the native variadic position in named argument variants, but
   only when all preceding param names align with native (preventing
   false positives for methods like PDO::query that don't support
   variadic-slot named params)

3. Enhances ParametersAcceptorSelector to perform name-based type
   matching when selecting among named argument variants, correctly
   rejecting variants whose required params don't match the call's
   named arguments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The base functionMap.php is used for PHP 7.4 and below where named
arguments do not exist. Only functionMap_php80delta.php needs the
PHP 8 parameter names for named argument support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Revert parameter names in functionMap.php to original PHP 7.4 names
  since this file is only for PHP 7.4 or less (named args don't exist)
- Use existing $typesCount variable instead of redundant count($types)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Invert the condition to reduce nesting and use $typesCount variable
instead of redundant count($types) call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-8ctq654 branch from 6e601fb to da44eea Compare May 10, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants