Skip to content

WP_Post_Type and WP_Taxonomy: describe the shape of the labels and capability objects - #477

Draft
swissspidy wants to merge 5 commits into
php-stubs:masterfrom
swissspidy:claude/phpstan-wordpress-typing-f9zirv
Draft

WP_Post_Type and WP_Taxonomy: describe the shape of the labels and capability objects#477
swissspidy wants to merge 5 commits into
php-stubs:masterfrom
swissspidy:claude/phpstan-wordpress-typing-f9zirv

Conversation

@swissspidy

@swissspidy swissspidy commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Description

WP_Post_Type::$labels, WP_Post_Type::$cap, WP_Taxonomy::$labels and WP_Taxonomy::$cap are declared as plain stdClass, so every label and capability read off them is mixed. WP_User::$caps, WP_User::$allcaps and WP_Role::$capabilities are bool[], which leaves their keys as array-key rather than the capability name.

These objects are built by get_post_type_labels(), get_post_type_capabilities(), get_taxonomy_labels() and WP_Taxonomy::set_props(), all of which fill in every default, so their contents can be described exactly.

Notes on the shapes

  • Labels whose default is null for one of the two hierarchies, as recorded in WP_Post_Type::get_default_labels() and WP_Taxonomy::get_default_labels(), are typed string|null. For example popular_items is null on a hierarchical taxonomy, and parent_item_colon is null on a non-hierarchical post type. Typing them string makes a legitimate null === $tax->labels->popular_items check report as always false.
  • menu_name and name_admin_bar are always present on both objects, even though get_taxonomy_labels() documents neither and get_post_type_labels() documents only menu_name. _get_custom_object_labels() sets name_admin_bar unconditionally, and menu_name is added to the defaults by both label builders. Core reads $taxonomy->labels->menu_name in wp-admin/menu.php.
  • The six capabilities get_post_type_capabilities() only adds under map_meta_cap, and the conditional template_name post type label, are listed as required rather than optional. map_meta_cap resolves to true for the default capability type (WP_Post_Type::set_props()), so they are present in practice, and an optional key in an object shape turns every direct read into an "undefined property" error.

Trade-off

Object shapes are sealed, so a custom label or capability registered on top of the defaults now needs its own annotation. That is already the case for get_post_type_capabilities() and get_taxonomy_labels(), whose return values are described the same way today.

Follow-up

The four property entries can be dropped once WordPress ships matching docblocks and the stubs are regenerated from that release, in the same way WP_Theme::get() was removed from the map in #475. A patch adding those docblocks to core is prepared separately; it also fixes the two undocumented labels and the nullability noted above.

See szepeviktor/phpstan-wordpress#274

`WP_Post_Type::$labels`, `WP_Post_Type::$cap`, `WP_Taxonomy::$labels` and
`WP_Taxonomy::$cap` are declared as plain `stdClass`, so every label and
capability read off them is `mixed`. The three capability maps are `bool[]`,
which leaves their keys as `array-key`.

The label and capability objects are built by `get_post_type_labels()`,
`get_post_type_capabilities()`, `get_taxonomy_labels()` and
`WP_Taxonomy::set_props()`, all of which fill in every default, so their
contents can be described exactly:

- Labels whose default is `null` for one of the two hierarchies, such as
  `popular_items` on a hierarchical taxonomy, are `string|null`.
- `menu_name` and `name_admin_bar` are always present, even though
  `get_taxonomy_labels()` does not list them. Core reads `menu_name` off a
  taxonomy in `wp-admin/menu.php`.
- The six capabilities that `get_post_type_capabilities()` only adds under
  `map_meta_cap`, and the conditional `template_name` post type label, are
  listed as required rather than optional: `map_meta_cap` ends up true for the
  default capability type, and an optional key would turn every direct read
  into an "undefined property" error.

Note that these object shapes are sealed, so a custom label or capability
registered on top of the defaults now needs its own annotation. That already
applies to `get_post_type_capabilities()` and `get_taxonomy_labels()`, whose
returns are described the same way.

The four property entries can be dropped once WordPress ships the matching
docblocks and the stubs are regenerated from that release, in the same way
`WP_Theme::get()` was removed from the map.

Refs szepeviktor/phpstan-wordpress#274

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122HqysjDiq2jSHebbSSu6A
@swissspidy
swissspidy force-pushed the claude/phpstan-wordpress-typing-f9zirv branch from 784e653 to 2230ad7 Compare August 21, 2026 09:20
@szepeviktor

Copy link
Copy Markdown
Member

You brought your robobuddy with you?

@swissspidy swissspidy changed the title Add conditional return types for WP_Query::query() and WP_Term_Query::query() WP_Post_Type and WP_Taxonomy: describe the shape of the labels and capability objects Aug 21, 2026
claude and others added 4 commits August 21, 2026 10:33
WordPress documents the contents of an object like `WP_Post_Type::$labels` on the
function that builds it, and refers to that function from the property with
`@see`. The visitor already follows that convention for `@param`, through
`discoverInheritedArgs()`, but not for `@var`, so a property stayed `stdClass`
however well the function it points at was documented. Describing those
properties in core therefore meant repeating a thirty-key list on the property,
which is exactly what the `@see` exists to avoid.

`@var` now follows the same reference. A property inherits the `@phpstan-return`
shape of a function it `@see`s when the two types agree, so `object` shapes only
land on `object` properties and `array` shapes on `array` ones. A shape of the
property's own always wins, whether it comes from hash notation in the source or
from the function map.

Generating the stubs for WordPress 7.0.1 before and after produces an identical
file: the four properties this was written for are still covered by their
function map entries, which take precedence, and nothing else in core both
carries a matching `@see` and lacks a shape of its own.

Refs szepeviktor/phpstan-wordpress#274

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122HqysjDiq2jSHebbSSu6A
php-stubs#478 added function map entries at the same spot in the file, between
`WP_REST_Request::set_param` and `WP_Theme`. Both sides are kept, in the
alphabetical order the file uses.

Its Composer and PHPCS changes are the same ones this branch already carried,
so those files came out identical to upstream. `wordpress-stubs.php` merged
cleanly, and regenerating it from the WordPress 7.0.1 tree reproduces the
merged file exactly, apart from the header and `$wpdb` shim that generate.sh
appends afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122HqysjDiq2jSHebbSSu6A
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