Skip to content

WP_Widget_Categories::widget(): Use booleans instead of numeric strings - #8071

Closed
IanDelMar wants to merge 24 commits into
WordPress:trunkfrom
IanDelMar:cat_args
Closed

IanDelMar wants to merge 24 commits into
WordPress:trunkfrom
IanDelMar:cat_args

Conversation

@IanDelMar

Copy link
Copy Markdown

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


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.

@github-actions

github-actions Bot commented Jan 1, 2025 •

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 marian1, westonruter.

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

@github-actions

github-actions Bot commented Jan 1, 2025

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

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • 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.

Comment thread src/wp-includes/widgets/class-wp-widget-categories.php Outdated
Co-authored-by: Weston Ruter <westonruter@gmail.com>
@westonruter

Copy link
Copy Markdown
Member

If it weren't for the widget_categories_dropdown_args filter which widened the type of $cat_args to array, the call below to wp_dropdown_categories() would emitting a PHPStan error:

Parameter #1 $args of function wp_dropdown_categories expects array{show_option_all?: string, show_option_none?: string, option_none_value?: string, orderby?: string, pad_counts?: bool, show_count?: bool|int, echo?: bool|int, hierarchical?: bool|int, ..., ...}|string, array{orderby: 'name', show_count: '0'|'1', hierarchical: '0'|'1', show_option_none: string, id: non-falsy-string} given.

With the changes here, $cat_args ends up typed as:

array{
    orderby: 'name', 
    show_count: bool, 
    hierarchical: bool, 
    show_option_none: string, 
    id: non-falsy-string
}

And no PHPStan error is raised.

Separately from the changes here, I want to improve the ApplyFiltersDynamicFunctionReturnTypeExtension for PHPStan so that if the @param of a filter is just a bare array that whatever the type of the value being passed into the filter then is used as the type instead. Alternatively, all the filter docs should be updated to specify the full array shape being passed in, which is clearly better long term. I've done that for the widget_categories_dropdown_args-filtered value here via d399c0b. Furthermore, the extension should flag an error when a value is passed in to such a filter but its analyzed type conflicts the @param.

* @type string $show_option_none Text to display for showing no categories. Default 'Select Category'.
* @type string $id Value for the 'id' attribute of the select element.
* }
* @param array $instance Array of settings for the current widget.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might as well type this as well

@westonruter westonruter Sep 4, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I found an issue with HashNotationVisitor which I want to fix as part of this. Currently, Keys of a @param hash are optional, at every level. But this is not always the case. For \WP_Widget_Categories::widget(), these args are always passed:

	 * @param array $args {
	 *     Display arguments.
	 *
	 *     @type string $before_widget HTML markup to prepend to each widget in the sidebar.
	 *     @type string $after_widget  HTML markup to append to each widget in the sidebar.
	 *     @type string $before_title  HTML markup to prepend to the widget title when displayed.
	 *     @type string $after_title   HTML markup to append to the widget title when displayed.
	 * }

Maybe there should be a way to denote “Required” args explicitly in this notation. Either this, or we just go ahead and use PHPStan notation instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm going ahead with adding this. We can address the new PHPStan errors (at higher rule levels) in a subsequent commit.

westonruter and others added 4 commits October 16, 2026 10:28
Describe the keys passed in `$cat_args` to the `widget_categories_args`
filter, matching the documentation already added for the
`widget_categories_dropdown_args` filter, including the boolean types of
`show_count` and `hierarchical`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add `@since 7.2.0` entries to the `widget_categories_dropdown_args` and
`widget_categories_args` filters, noting that `show_count` and
`hierarchical` are now passed as booleans rather than numeric strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@westonruter

westonruter commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

🤖 Comment by Claude Opus 5.5

PHPStan impact at rule level 10

Updated for 5d41db6. The earlier version of this comment covered 3eddf65, before the widget instance, update(), form() and $args were documented.

PHPStan was run on trunk (the merge base) and on this branch at 5d41db6. Both runs used phpstan.neon at rule level 10, with the result cache wiped before each run. The analysis covers src/ only, so the test changes are not included.

Metric Value
Errors in trunk 27,460
Errors in cat_args 27,453
Net change −7
Errors fixed 14
Errors introduced 7

All of the changes are in src/wp-includes/widgets/class-wp-widget-categories.php, which goes from 18 errors to 11.

Fixed (14)

  • 6 × missingType.iterableValue: widget()'s $args and $instance, update()'s $new_instance, $old_instance and return value, and form()'s $instance now have value types.
  • 2 × echo.nonString and 2 × binaryOp.invalid: before_widget, after_widget, before_title and after_title are now typed as string.
  • 2 × argument.type: the sanitize_text_field() call in update(), and the wp_parse_args() call in form().
  • 2 × argument.type: the wp_dropdown_categories() and wp_list_categories() calls. These two are reworded rather than fixed. They appear again under "Introduced".

Introduced (7)

  • 2 × argument.type (reworded): the wp_dropdown_categories() and wp_list_categories() errors from trunk, now reporting the hook's documented shape in place of a plain array. HashNotationVisitor leaves @param hashes open (trailing ...), because a filter callback may add keys. PHPStan therefore cannot rule out an extra key, such as echo, holding a value the receiving function does not accept. A probe confirmed that the same keys in a closed shape are accepted.
  • 4 × offsetAccess.notFound: reads of $args['before_widget'], ['after_widget'], ['before_title'] and ['after_title']. Core always passes these keys, but HashNotationVisitor translates every key of a @param hash as optional, and hash notation has no way to mark a key as required.
  • 1 × return.type: update() is documented as returning array{title: string, count: 0|1, hierarchical: 0|1, dropdown: 0|1}, which matches exactly, except that the returned array is open. update() starts from $old_instance, whose open @param shape allows other keys, while a @return hash is closed.

The 5 errors that are actually new are not problems in the code. They come from two limits of how hash notation is translated: there is no way to mark @param keys as required, and an open input shape cannot flow into a closed return shape. Those are better addressed in the tooling separately than worked around here.

A real case that this kind of analysis exposed was fixed in 8697d5b: update() read $new_instance['title'] without checking it was set, which emits an undefined-key warning when REST form data omits title.

@westonruter

Copy link
Copy Markdown
Member

🤖 Comment by Claude Opus 5.5

Back-compat review

This PR changes show_count and hierarchical in the Categories widget's $cat_args from the strings '1'/'0' to true/false. Only code that checks the exact type sees a difference, for example === '1', === true or is_string(). Loose comparisons such as == 1 or == '1', and plain truthiness checks, behave the same as before.

Core

No impact. wp_list_categories(), wp_dropdown_categories(), Walker_Category, Walker_CategoryDropdown and WP_Term_Query only check whether these values are truthy. The Categories block (src/wp-includes/blocks/categories.php) already passes booleans for both keys, so the widget now matches the block.

Plugins and themes (Veloria)

Callbacks on the two widget filters. About 40 plugins and 50 themes in the WordPress.org directories reference widget_categories_args or widget_categories_dropdown_args.

  • Most callbacks only add exclude, class or a custom walker.
  • Many of the other matches are copies of the widget that call the filters themselves with their own $cat_args. This PR does not change what those copies pass.
  • The callbacks that do read these keys only check truthiness:
    • Category Commander: ! empty( $args['hierarchical'] )
    • The Customify, Nevo and Starter Blog walkers: ! empty( $args['show_count'] )
    • Shiword overwrites hierarchical instead of reading it.

Strict comparisons anywhere. A search was run for show_count or hierarchical compared against 0 or 1 (string or integer) with ===, !==, == or !=, in both normal and Yoda order. It returned about 40 matches. Only one of them involves these widget arguments:

  • Hideo (around 400 active installs), functions.php:242:
    • It filters the output of wp_list_categories, and when $args['show_count'] === true it changes </a> (5) into </a><span class="count">5</span>.
    • At the moment only the Categories block passes true, so only the block gets this markup. With this PR, the classic widget gets it too.
    • Hideo styles .count only under .wp-block-categories, so post counts in the classic widget would lose their parentheses and appear unstyled next to the category name.
    • The impact is small and may be what the theme intended, but it is a visible change.

Other minor differences

  • A callback that converts these values to a string (in a data attribute or a cache key, for example) now gets '' instead of '0' when the setting is off. No such callback was found, but this case was not searched for specifically.
  • WP_Term_Query builds its cache key from the serialized query arguments. Because the type of hierarchical changes, each widget will get one cache miss after the upgrade, which is harmless.

Conclusion

No breaking change was found. The only visible effect is the Hideo markup change described above.

westonruter and others added 12 commits October 16, 2026 11:22
Describe the `title`, `count`, `hierarchical` and `dropdown` keys of the
widget instance wherever it is passed: `widget()`, `form()`, `update()`
(including the submitted settings and the returned array), and the
`$instance` parameter of the `widget_categories_dropdown_args` and
`widget_categories_args` filters.

The flags are documented as `int|string` on input, since `update()` saves
them as `0` or `1` but `the_widget()` can pass them as strings parsed
from a query string.

This removes 7 PHPStan errors at rule level 10 in the file. It surfaces
2 others in `update()`, which reflect limits of how hash notation is
translated rather than wrong documentation: keys of a `@param` hash are
always optional, so reading `$new_instance['title']` is flagged, and a
`@return` hash is sealed, while `update()` also carries over any other
keys in `$old_instance`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Narrow `count`, `hierarchical` and `dropdown` in the documented widget
instance from `int|string` to `int`, which is what `update()` saves.
A string only arrives when `the_widget()` is given a query string, which
`wp_parse_args()` turns into string values, and that usage is not one the
types should accommodate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`update()` only ever stores `count`, `hierarchical` and `dropdown` as
`0` or `1`, so document them with that literal type rather than `int`,
which also makes the "Accepts 0 or 1" text redundant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A checkbox in the widget form has no `value` attribute, so it submits
`'on'` when checked and is omitted when unchecked. The block-based
widget editor also resubmits the saved instance through the REST API,
which passes the stored `0` or `1` to `update()` as `$new_instance`.
Document both instead of a generic `string`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The documented instance types say `count`, `hierarchical` and `dropdown`
are `0|1`, so note why `widget()` still reads them with `! empty()`:
the keys are missing on an unsaved instance, instances saved before
WordPress 2.8 (r10809) stored booleans, and `the_widget()` passes values
through as its caller supplies them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the `before_widget`, `after_widget`, `before_title` and
`after_title` keys of the `$args` passed to `widget()`.

This removes 5 PHPStan errors at rule level 10 and surfaces 4
`offsetAccess.notFound` errors where those keys are read. Keys of a
`@param` hash are always translated as optional, while core always
passes these four, so the new errors reflect that limit of the hash
notation tooling rather than a problem in the code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`MockAction::get_args()` returns `mixed`, so PHPStan reported every
offset read on the captured `$cat_args` and its `array_keys()` call.
Assert that it is an array first, which narrows the type and checks
that the filter received one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `widget_categories_args` and `widget_categories_dropdown_args` tests
cast `show_count` and `hierarchical` to `bool` before asserting, so they
would still pass if `widget()` went back to passing `'1'` and `'0'`.
Compare the raw values so the type change is covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `widget_categories_dropdown_args` filter's `$cat_args` go to
`wp_dropdown_categories()`, which documents options such as
`show_option_all`, `selected` and `value_field` that `WP_Term_Query`
does not. Reference it for the additional accepted arguments, as the
`widget_categories_args` docs do with `wp_list_categories()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
westonruter and others added 2 commits October 16, 2026 12:04
The widget always sets `show_option_none` to 'Select Category'; it is
not a fallback. If a callback removes it, `wp_dropdown_categories()`
uses its own default of an empty string, so the option disappears.
Describe the value as set rather than as a default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Instances saved before WordPress 2.8 stored `count` and `hierarchical`
as booleans, which is one reason `widget()` reads them with `! empty()`.
Cover both `true` and `false` in the widget options data provider so
the filter-args and list-output tests exercise that back-compat case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Add coverage for updates where the widget title is omitted.

0 open findings

What changed in this PR

Updates WP_Widget_Categories to use boolean category arguments while preserving legacy widget compatibility.

Changes:

  • Converts category flags to booleans.
  • Handles missing widget titles safely.
  • Adds compatibility and boolean-behavior tests.
File Summary
tests/​phpunit/​tests/​widgets/​wpWidgetCategories.php Verifies boolean arguments and legacy instances.
src/​wp-includes/​widgets/​class-wp-widget-categories.php Converts widget flags and updates documentation.

🧠 Review effort: Lite


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

westonruter and others added 2 commits October 16, 2026 12:23
`update()` falls back to an empty title when `$new_instance` has no
`title` key, as when REST form data omits it. Add a data provider case
for that, which fails with an undefined array key warning if the
fallback is removed, and which also checks that the old title is
cleared rather than kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wporg-sync pushed a commit that referenced this pull request Oct 16, 2026
The Categories widget passed `show_count` and `hierarchical` to `wp_list_categories()` and `wp_dropdown_categories()` as the numeric strings `'0'` and `'1'`, while both functions document these arguments as `bool|int`. They are now passed as booleans, matching what the Categories block already passes. Core only checks these values for truthiness, so output is unchanged; a callback on the `widget_categories_args` or `widget_categories_dropdown_args` filter that compares them strictly to `'1'` or `'0'` is the only code affected, which is noted in the filters' `@since` tags.

The arguments passed to those filters, along with the widget's display arguments and instance settings, are now documented as hashes so that static analysis can type them. The `count`, `hierarchical` and `dropdown` settings are documented as the `0` or `1` that `update()` stores, while `widget()` keeps reading them loosely for instances saved before WordPress 2.8, which stored booleans. A missing title in the submitted settings no longer triggers an undefined array key warning in `update()`, which can happen when REST API form data omits it.

Developed in #8071.
Follow-up to r10809, r41685, r64331.

Props westonruter, marian1.
See #65817, #65860.
Fixes #62762.


git-svn-id: https://develop.svn.wordpress.org/trunk@64413 602fd350-edb4-49c9-b593-d223f7449a82
@github-actions

Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 64413
GitHub commit: 9dececd

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions Bot closed this Oct 16, 2026
wporg-sync pushed a commit to WordPress/WordPress that referenced this pull request Oct 16, 2026
The Categories widget passed `show_count` and `hierarchical` to `wp_list_categories()` and `wp_dropdown_categories()` as the numeric strings `'0'` and `'1'`, while both functions document these arguments as `bool|int`. They are now passed as booleans, matching what the Categories block already passes. Core only checks these values for truthiness, so output is unchanged; a callback on the `widget_categories_args` or `widget_categories_dropdown_args` filter that compares them strictly to `'1'` or `'0'` is the only code affected, which is noted in the filters' `@since` tags.

The arguments passed to those filters, along with the widget's display arguments and instance settings, are now documented as hashes so that static analysis can type them. The `count`, `hierarchical` and `dropdown` settings are documented as the `0` or `1` that `update()` stores, while `widget()` keeps reading them loosely for instances saved before WordPress 2.8, which stored booleans. A missing title in the submitted settings no longer triggers an undefined array key warning in `update()`, which can happen when REST API form data omits it.

Developed in WordPress/wordpress-develop#8071.
Follow-up to r10809, r41685, r64331.

Props westonruter, marian1.
See #65817, #65860.
Fixes #62762.

Built from https://develop.svn.wordpress.org/trunk@64413


git-svn-id: http://core.svn.wordpress.org/trunk@63563 1a063a9b-81f0-0310-95a4-ce76da25c4cd
@IanDelMar
IanDelMar deleted the cat_args branch October 18, 2026 08:00
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