Skip to content

Return list<mixed> from PDOStatement::fetchAll()#5643

Merged
VincentLanglet merged 5 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-e4psdj6
May 15, 2026
Merged

Return list<mixed> from PDOStatement::fetchAll()#5643
VincentLanglet merged 5 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-e4psdj6

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

@phpstan-bot phpstan-bot commented May 11, 2026

Comment thread tests/PHPStan/Analyser/nsrt/bug-11889.php Outdated
@staabm staabm requested a review from VincentLanglet May 12, 2026 05:46
Comment thread resources/functionMap.php Outdated
Comment thread resources/functionMap.php Outdated
@staabm staabm changed the title Return list<mixed> from PDOStatement::fetchAll(), pg_fetch_all(), and pg_fetch_all_columns() in function signature map Return list<mixed> from PDOStatement::fetchAll() in function signature map May 12, 2026
Comment thread tests/PHPStan/Analyser/nsrt/bug-11889.php Outdated
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.

$stmt->fetchAll(\PDO::FETCH_COLUMN); is a valid call and returns a list.

Since both

$stmt->fetchAll(PDO::FETCH_ASSOC)
$stmt->fetchAll(\PDO::FETCH_COLUMN);

fallback to the same signature, I think we cannot solve this by changing the functionMap, but with a dedicated returnTypeExtension instead.

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. Here's what I did to address VincentLanglet's review:

Created src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php — a DynamicMethodReturnTypeExtension that inspects the constant fetch mode argument of PDOStatement::fetchAll() to determine whether the result is a list or array:

  • Returns list for modes like FETCH_ASSOC, FETCH_NUM, FETCH_BOTH, FETCH_OBJ, FETCH_COLUMN, FETCH_CLASS, FETCH_NAMED, etc.
  • Returns array (falls back to function map) for FETCH_KEY_PAIR, and any mode with FETCH_GROUP or FETCH_UNIQUE flags set
  • Returns array for no-argument calls or non-constant mode values

This solves the reviewer's concern that fetchAll(PDO::FETCH_COLUMN) and fetchAll(PDO::FETCH_ASSOC) both match the same function map signature but should return different types (both are actually lists, while FETCH_KEY_PAIR/FETCH_GROUP are not).

Updated tests/PHPStan/Analyser/nsrt/bug-11889.php with comprehensive assertions covering single-arg list modes, non-list modes (KEY_PAIR, GROUP|ASSOC, UNIQUE|ASSOC), and multi-arg overload variants.

@VincentLanglet VincentLanglet changed the title Return list<mixed> from PDOStatement::fetchAll() in function signature map Return list<mixed> from PDOStatement::fetchAll() May 15, 2026
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.

Test are failing on PHP 8.5

PHPStan think

assertType('array', $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC));
	assertType('array', $stmt->fetchAll(PDO::FETCH_UNIQUE | PDO::FETCH_ASSOC));

will return a list ; why ?

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. The fix changes the extension to explicitly return array<mixed, mixed> for non-list fetch modes (FETCH_DEFAULT, FETCH_KEY_PAIR, FETCH_GROUP, FETCH_UNIQUE) instead of returning null and relying on the fallback. The fallback was selecting named argument variants on PHP 8.5 that only had a mode parameter and returned list<mixed>, which is why FETCH_GROUP|FETCH_ASSOC and FETCH_UNIQUE|FETCH_ASSOC were incorrectly inferred as list instead of array.

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.

Why On PHP 8.5, the named variants for fetchAll() have only a mode parameter ?

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Now I have the full picture. Here's my analysis and response:

The named argument variant truncation is NOT PHP 8.5-specific. It happens on all PHP 8.0+ because:

  1. The native stub for PDOStatement::fetchAll is fetchAll(int $mode = PDO::FETCH_DEFAULT, mixed ...$args) across all PHP 8.x versions (from vendor/phpstan/php-8-stubs)
  2. In Php8SignatureMapProvider::getMergedSignatures() (line 296-297), when building named argument variants, the loop breaks at variadic native parameters (...$args is variadic at index 1)
  3. All 4 named variants are truncated to just [mode] — variant 0 returns array, variants 1-3 return list<mixed>

The previous commit message saying "on PHP 8.5" was misleading. The truncation happens on all PHP 8.0+ versions. However, it was harmless before this PR because all variants returned array. When variants 1-3 were changed to list<mixed>, the fallback could select a list<mixed> truncated named variant.

The fix is correct regardless: by explicitly returning array<mixed, mixed> for non-list fetch modes in the extension (instead of returning null and relying on fallback variant selection), we avoid any issues with named variant selection on any PHP version.

There are no code changes to make — tests pass, phpstan passes, and the fix is already in place. The reviewer's question was about clarifying the PHP 8.5-specific claim from the previous commit message, and the answer is that the behavior is not PHP 8.5-specific — it applies to all PHP 8.0+ versions.

@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-e4psdj6 branch from cbbfa11 to 6500e71 Compare May 15, 2026 11:13
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.

In PHP 8.5, the values of PDO::FETCH_GROUP, PDO::FETCH_UNIQUE, PDO::FETCH_CLASSTYPE, PDO::FETCH_PROPS_LATE, and PDO::FETCH_SERIALIZE have changed.

FETCH_GROUP is 32 on PHP 8.5

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. Here's what the fix does:

The hardcoded FETCH_KEY_PAIR = 12 and FETCH_GROUP = 0x10000 constants were incorrect on PHP 8.5, where PDO constants changed values (e.g., FETCH_GROUP became 32 instead of 65536). The extension now resolves PDO::FETCH_KEY_PAIR, PDO::FETCH_GROUP, and PDO::FETCH_UNIQUE dynamically via InitializerExprTypeResolver, which reads the actual values from BetterReflection for whatever PHP version is being analyzed. This also adds an explicit FETCH_UNIQUE check (previously it was implicitly caught by the FETCH_GROUP bitmask on pre-8.5, but that relationship may not hold on 8.5).

@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-e4psdj6 branch 3 times, most recently from 86525b2 to d6ee247 Compare May 15, 2026 12:23
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-e4psdj6 branch from d6ee247 to baf366c Compare May 15, 2026 12:23
@VincentLanglet VincentLanglet requested a review from staabm May 15, 2026 12:49
@VincentLanglet VincentLanglet self-assigned this May 15, 2026
Comment thread src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php
On PHP < 8.0, PDOStatement::fetchAll() returns array|false.
The extension's false-union branch was untested. This adds a
lint < 8.0 test covering list<mixed>|false for list modes and
array|false for non-list modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread tests/PHPStan/Analyser/nsrt/bug-11889-php7.php Outdated
@VincentLanglet VincentLanglet merged commit 86f8b7a into phpstan:2.1.x May 15, 2026
658 of 662 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-e4psdj6 branch May 15, 2026 13:53
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.

3 participants