Skip to content

Docs: Clarify return value semantics of wpdb query methods#11855

Open
apermo wants to merge 9 commits into
WordPress:trunkfrom
apermo:trac-65261-wpdb-get-results-phpdoc
Open

Docs: Clarify return value semantics of wpdb query methods#11855
apermo wants to merge 9 commits into
WordPress:trunkfrom
apermo:trac-65261-wpdb-get-results-phpdoc

Conversation

@apermo

@apermo apermo commented May 18, 2026

Copy link
Copy Markdown

Clarifies the inline documentation for the four wpdb query methods (get_results(), get_row(), get_col(), get_var()) and adds PHPStan annotations where the runtime dispatch branches by parameter value.

Per-commit summary:

  1. get_results() — descriptive text. Documents that an empty array is returned when no rows match or on a database error, and that null is returned only when $query is empty or $output is not one of the recognized constants. Lifts the developer.wordpress.org wording into the inline docblock.
  2. get_results() — PHPStan + type tightening. Adds a @phpstan-return conditional type that mirrors the runtime dispatch on $query and $output. Refines @param string $query to string|null (matches get_row()'s existing form). Tightens @return array|object|null to array|null — the method never returns a bare stdClass; the object in the union appears to be a copy/paste artefact from get_row().
  3. get_row() — PHPStan. Adds a @phpstan-return conditional type for the per-$output return shapes. @return array|object|null is already accurate here and is left untouched.
  4. get_col() — PHPStan. Adds @phpstan-return list<string|null> to tighten the element type. Each element is the result of a get_var() call.
  5. get_var() — descriptive text. Clarifies that null is also returned when the matched cell value is an empty string (Trac #30257 documented this as intended behavior years ago, but the docblock has continued to say "null on failure" only). Points consumers at $this->last_error for distinguishing the two cases.
  6. get_row() and get_results()@phpstan-param. Pins $output to the literal-string union of the documented constants. Lets PHPStan flag callers that pass typos or the undocumented lowercase form (e.g. 'object'). Core has no such callers.
  7. get_results() — refined return shapes. Splits OBJECT and OBJECT_K (list<stdClass> vs array<array-key, stdClass> — they have demonstrably different shapes). Switches ARRAY_A/ARRAY_N outer types from array<int, ...> to list<...> to match the $new_array[] population pattern.
  8. get_var() — prose hoist. Moves the empty-string-vs-failure explanation and the $this->last_error pointer out of the @return line and into the description block above @param, matching WP-core convention of terse @return lines with the rationale up top.
  9. Style polish. Drops the blank line between @return and @phpstan- tags across all three updated docblocks, matching the formatting of the existing @phpstan-return precedent on wp_parse_url() in wp-includes/http.php.

None of the commits change behavior; all are docblock-only.

The case-insensitive $output back-compat path (strtoupper( $output ) === OBJECT) is intentionally not modelled in the PHPStan annotations. It is hard to express in PHPStan without going into regex territory, and anyone relying on lowercase 'object' is using undocumented behavior. It folds into the else branch and types as null. Commit 6's @phpstan-param narrowing makes that visible to static analysis.

Trac ticket: https://core.trac.wordpress.org/ticket/65261

The ticket originally scoped to get_results() only. Per discussion with @westonruter on this PR, the scope was broadened to cover all four query methods. The Trac ticket title/description should be updated to match.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.7
Used for: Source-code trace of each query method's failure paths, comparison of the inline docblocks against the developer.wordpress.org reference and against existing PHPStan precedent in core (wp_parse_url() in wp-includes/http.php), drafting of the revised docblock wording and PHPStan conditional types, an independent fresh-context code review of the resulting diff that surfaced the OBJECT/OBJECT_K shape split and the get_var() prose hoist, and drafting of this PR description and the Trac ticket. All output was reviewed by me; I take responsibility for the change.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

The `@return` line previously documented only the union type
`array|object|null`, without stating which branch is returned in
which situation. The user-facing reference on developer.wordpress.org
already explains this correctly; this brings the inline docblock in
line with that documentation.

No behaviour change.

See #65261.
@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props apermo, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@westonruter

Copy link
Copy Markdown
Member

Can we go the extra mile and provide a @phpstan-return conditional types for the branches? For this part, for example:

If the $query string is empty or you pass an invalid output_type, NULL will be returned.

I think this would be:

( $query is null|''|false|0 ) ? null : ( $output_type is 'ARRAY_A'|'ARRAY_N'|'OBJECT_K'|'OBJECT' ) ? ( ... ) : null

Where ... contains the branches for the valid parameters.

@westonruter westonruter self-requested a review May 19, 2026 18:14
@apermo

apermo commented May 26, 2026

Copy link
Copy Markdown
Author

Yeah, makes sense.

A couple of small things while drafting it:

  • The $query param is typed string in the docblock but the default is null, so the honest type is string|null (which get_row() already uses). The cleanest way to express the falsy branch is $query is non-falsy-string, which covers '', '0', and null without enumerating them.
  • The $output constants are literal strings (OBJECT, OBJECT_K, ARRAY_A, ARRAY_N), so literal matching works directly.
  • The case-insensitive back-compat path (strtoupper( $output ) === OBJECT) is hard to express in PHPStan without going into regex territory. I would lean toward modeling the documented contract and letting that path fold into the else branch. Anyone relying on the back-compat is using undocumented behavior anyway.

Here is what I have so far:

@phpstan-param string|null $query
@phpstan-return (
    $query is non-falsy-string
        ? (
            $output is 'OBJECT'|'OBJECT_K'
                ? array<int|string, stdClass>
                : (
                    $output is 'ARRAY_A'
                        ? array<int, array<string, mixed>>
                        : (
                            $output is 'ARRAY_N'
                                ? array<int, list<mixed>>
                                : null
                        )
                )
        )
        : null
)

While looking at this I noticed something else. The current @return array|object|null overstates the type. get_results() never actually returns a bare stdClass. Every branch returns either an array of stdClass rows, an array of arrays, or null. The object in the union looks like a copy-paste leftover from get_row() (which does return one stdClass). I would want to fix that to @return array|null in the same patch.

Two questions:

  1. Do you want this in #65261, or should I split it into a follow-up so this PR stays narrowly scoped on the descriptive text? Either is fine with me.
  2. The same kind of imprecise @return and missing conditional types is almost certainly there on get_row(), get_col(), and get_var() too. If you are up for a broader sweep, I am happy to take that on as well. Would you prefer this as a follow-up ticket or as part of this PR?

@westonruter

Copy link
Copy Markdown
Member
  1. Do you want this in #65261, or should I split it into a follow-up so this PR stays narrowly scoped on the descriptive text? Either is fine with me.

I think it's preferable to fix up PHPStan issues in the course of making other related changes. It reduces the amount of work to make commits.

2. The same kind of imprecise @return and missing conditional types is almost certainly there on get_row(), get_col(), and get_var() too. If you are up for a broader sweep, I am happy to take that on as well. Would you prefer this as a follow-up ticket or as part of this PR?

Yeah, I think a PR that fixes up the return semantics of all methods on WPDB would be good. So if anything, the ticket scope can be changed.

apermo added 4 commits June 12, 2026 12:32
Adds a `@phpstan-return` conditional type that mirrors the runtime
dispatch on `$query` and `$output`, and corrects two longstanding
docblock inaccuracies on the surrounding tags:

* `@param string $query` becomes `@param string|null $query`. The
  method signature defaults `$query` to `null`, so `string|null` is
  the honest type. `wpdb::get_row()` already uses this form.
* `@return array|object|null` becomes `@return array|null`. The
  method never returns a bare `stdClass`; every branch returns an
  array of rows (of `stdClass`, associative arrays, or numeric
  arrays) or `null`. The `object` in the union appears to be a
  copy/paste artefact from `wpdb::get_row()`.

The case-insensitive `$output` back-compat path is intentionally
not modelled. Anything outside the documented constants folds into
the `else` branch and types as `null`.

No behaviour change.

See #65261.
Documents the per-`$output` return shapes via a PHPStan
conditional type that mirrors the runtime dispatch.

The existing `@return array|object|null` already accurately
describes the possible types, so it is left untouched. The
case-insensitive `$output` back-compat path is not modelled;
it folds into the `else` branch and types as `null`.

No behaviour change.

See #65261.
Tightens the element type via PHPStan. Each element of the
returned list is the value of column `$x` for a row in the
last result, fetched through `wpdb::get_var()`, which returns
`string|null`. The list is sequentially indexed from 0, so the
refined type is `list<string|null>`.

The human-readable `@return array` line is left untouched.

No behaviour change.

See #65261.
The implementation returns `null` not only on query failure but
also when the matched cell value is an empty string. The
existing "or null on failure" wording masks the empty-string
case, which has caused repeated confusion (see Trac #30257 and
the Documentation-Issue-Tracker entry for `get_var()`).

This documents the empty-string path and points consumers that
need to distinguish failure from an empty result at
`$this->last_error`. No conditional `@phpstan-return` is added;
the return type does not vary with `$query` or `$x`/`$y`.

No behaviour change.

See #65261.
@apermo apermo changed the title Docs: Clarify return value semantics of wpdb::get_results() Docs: Clarify return value semantics of wpdb query methods Jun 12, 2026
@apermo

apermo commented Jun 12, 2026

Copy link
Copy Markdown
Author

Pushed four follow-up commits, one per method.

  • get_results(): PHPStan conditional + @return array|null + @param string|null (cdd144a)
  • get_row(): PHPStan conditional (31989de)
  • get_col(): PHPStan list<string|null> element type (72a1952)
  • get_var(): clarified that null is also returned when the matched value is an empty string, per Trac #30257 (f89caa9)

Verified with composer phpstan -- src/wp-includes/class-wpdb.php: no new errors, the two preexisting ones (lines 1923/1924, flush() assigning null to non-nullable col_info/last_query) are unrelated and show up on trunk without my changes too.

PR title and description are updated to reflect the broader scope. The Trac ticket still says get_results() only in its title and description. Happy to add a comment over there summarising the scope change, or to update the title directly. Whichever way works for you.

Take a look whenever.

apermo added 4 commits June 13, 2026 16:15
…`wpdb::get_results()`.

Pins `$output` to the literal-string union of the documented
constants. Lets PHPStan flag callers that pass typos or the
undocumented lowercase form (e.g. `'object'`), which currently
works only through the case-insensitive back-compat path in
each method.

`wpdb::get_results()` accepts `OBJECT`, `OBJECT_K`, `ARRAY_A`,
or `ARRAY_N`. `wpdb::get_row()` accepts `OBJECT`, `ARRAY_A`,
or `ARRAY_N` (no `OBJECT_K`).

No core callers use the lowercase form. No behaviour change.

See #65261.
…s()`.

`OBJECT` and `OBJECT_K` produce different array shapes and were
previously collapsed under one branch:

* `OBJECT` returns `$this->last_result`, which is populated by
  `$this->last_result[ $num_rows ] = $row;` and is therefore a
  zero-indexed `list<stdClass>`.
* `OBJECT_K` builds a fresh array keyed by the value of each
  row's first column, which is `array<array-key, stdClass>` and
  is not a list (keys can be string or int, duplicates are
  discarded).

`ARRAY_A` and `ARRAY_N` similarly populate `$new_array[]`, so
their outer types are `list<...>` rather than `array<int, ...>`.

No behaviour change.

See #65261.
The previous `@return` line ran to four sentences, which is at
odds with WP-core convention of keeping `@return` terse and
moving the rationale into the description block.

Moves the empty-string-vs-failure explanation and the
`$this->last_error` pointer into the prose paragraph above
`@param`, and tightens the `@return` line to a single sentence
that still mentions both null causes.

No behaviour change.

See #65261.
Matches the formatting of the existing `@phpstan-return`
precedent on `wp_parse_url()` in `wp-includes/http.php`, where
the `@phpstan-` tags follow `@return` directly without an
empty line between them.

Applied to all three docblocks that now carry `@phpstan-return`
in this class: `wpdb::get_row()`, `wpdb::get_col()`, and
`wpdb::get_results()`.

No behaviour change.

See #65261.
@apermo

apermo commented Jun 13, 2026

Copy link
Copy Markdown
Author

Pushed four more commits after running the diff through a fresh independent review.

  • 522405ae95: @phpstan-param narrowing for $output on get_row() and get_results(). Pins to the documented constants and lets PHPStan catch typos / lowercase. No core callers use the lowercase form, so no baseline work needed.
  • ba0a402929: split OBJECT vs OBJECT_K in get_results() (list<stdClass> vs array<array-key, stdClass>, they have different shapes), and switch ARRAY_A/ARRAY_N outer types to list<...> to match the $new_array[] population.
  • fad48753c8: hoisted the get_var() empty-string-vs-failure explanation out of @return and into the description block. The @return line is now one sentence, matching WP-core style.
  • 9655035b38: dropped the blank line between @return and @phpstan- tags so it matches wp_parse_url()'s formatting in http.php.

PHPStan verified again: no new errors. The two preexisting ones in flush() still show up on trunk without my changes.

PR description updated to reflect all nine commits. Take another look whenever.

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