Skip to content

Comments: Introduce a register_comment_type() API - #12311

Open
adamsilverstein wants to merge 24 commits into
WordPress:trunkfrom
adamsilverstein:feature/register-comment-type
Open

adamsilverstein wants to merge 24 commits into
WordPress:trunkfrom
adamsilverstein:feature/register-comment-type

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jun 24, 2026 •

Copy link
Copy Markdown
Member

Stack: Custom Comment Types (#35214)

PR Change Builds on
👉 #12311 register_comment_type() registration API (this PR) trunk
#51 REST /wp/v2/comment-types endpoint #12311
#52 Capabilities object (advisory) #12311
#55 Enforce type capabilities in map_meta_cap() adamsilverstein#52
#56 Type-aware REST moderation adamsilverstein#55
#57 Type-aware admin comment dimming adamsilverstein#56
#53 render_callback display callback #12311
#54 is_ping ping grouping adamsilverstein#53
#12310 default_excluded_comment_types filter trunk
#58 wp_update_comment_counts() recount helper #12310

#12311 and #12310 are on WordPress/wordpress-develop; the rest are fork PRs targeting their parent branch and will retarget to trunk as their base lands.

Claude rewrote this description after a fresh review pass:

What

Adds a comment type registration API modeled on post types and taxonomies: register_comment_type() and friends, a WP_Comment_Type class, and registrations for the built-in comment, pingback, trackback, note, and reaction types. Registration provides labels and metadata only. It doesn't constrain what gets stored in comment_type and makes no change to WP_Comment_Query.

Why

Trac #35214 is the long-running ticket for custom comment types, and the consensus there has been "backend first, smaller steps." The storage groundwork shipped in 5.5 (#49236, comments store comment_type = 'comment' instead of ''). This is the next slice, and the base the rest of the stack builds on (REST, capabilities, render callbacks).

See https://core.trac.wordpress.org/ticket/35214

How

  • src/wp-includes/class-wp-comment-type.php - new WP_Comment_Type, a trimmed mirror of WP_Taxonomy. Args: label, labels, description, public, internal, _builtin.
  • src/wp-includes/comment.php - register_comment_type(), unregister_comment_type(), get_comment_type_object(), get_comment_types(), comment_type_exists(), get_comment_type_labels() (reuses _get_custom_object_labels() and strips the post-type-only labels it derives), and create_initial_comment_types(), hooked on init (priority 0) and change_locale.
  • note and reaction are registered as internal, non-public built-ins, matching how core already treats them.
  • Built-in types can't be re-registered or unregistered (WP_Error plus _doing_it_wrong()). That's stricter than register_post_type(), but a new API has no legacy re-registration to preserve, and overwriting a built-in could strip flags core relies on. Custom types keep silent-overwrite semantics.
  • all, comments, and pings are reserved, since WP_Comment_Query reads them as query tokens.
  • comment_type() in comment-template.php falls back to a registered non-built-in type's singular label when no custom text is passed. Built-in output and explicit overrides are unchanged.
  • Hooks follow post type/taxonomy conventions: register_comment_type_args, register_{$type}_comment_type_args, comment_type_labels_{$type}, registered_comment_type, registered_comment_type_{$type}, unregistered_comment_type.
  • Each argument drives one layer of behavior and never implies another: public is display intent, internal is query/count exclusion intent (acted on through default_excluded_comment_types in Comments: Add filter for comment types excluded from queries by default #12310). The one cascade planned is show_in_rest defaulting from public. Like post types, the registry is per-process, not per-site on multisite.
  • WP_UnitTestCase_Base::reset_comment_types() empties the registry and re-runs create_initial_comment_types() between tests.

Testing

Test in WordPress Playground

  • Run vendor/bin/phpunit --group comment (single site and multisite). New tests live in tests/phpunit/tests/comment/types.php, wpCommentType.php, and commentType.php.
  • In a mu-plugin, register a type on init: register_comment_type( 'review', array( 'labels' => array( 'name' => 'Reviews', 'singular_name' => 'Review' ) ) );. Check get_comment_types() includes it and comment_type() prints "Review" for a comment with that type.
  • Try register_comment_type( 'pingback', array() ) and unregister_comment_type( 'note' ) - both should return a WP_Error and leave the built-in in place.
  • Check get_comment_types( array( 'internal' => true ) ) returns note and reaction.

Out of scope / follow-ups

  • Acting on internal in queries and counts is Comments: Add filter for comment types excluded from queries by default #12310 (default_excluded_comment_types).
  • REST exposure, capabilities, render callbacks, and ping grouping are the follow-up PRs in this stack.
  • Admin list table and dropdown integration is waiting on design feedback per #35214. A show_ui argument was dropped until that work needs it, since it's easy to add later and impossible to remove after release.
  • Some edges are pinned by tests rather than blocked: a plugin passing _builtin bypasses the re-registration guard and locks its own type (the same exposure register_post_type() has).

Test in WordPress Playground

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

AI Use

Code and description both written with 🤖 Claude Code. I will review and test.

Add a comment type registration API modeled on the post type and taxonomy
APIs, a first step toward custom comment types. Introduce the WP_Comment_Type
class and register_comment_type(), unregister_comment_type(),
get_comment_type_object(), get_comment_types(), and comment_type_exists().

Register the built-in comment, pingback, trackback, and note types via
create_initial_comment_types(), hooked on init at priority 0 and on
change_locale so labels re-translate. The note type is marked internal.

Make the comment_type() template tag fall back to a registered type's
singular label for non-built-in types when no custom text is supplied.
Built-in output and explicit overrides are unchanged.

Registration provides labels and metadata only; it does not constrain the
values stored in the comment_type column or alter WP_Comment_Query behavior.

See #35214.
Cover register_comment_type(), unregister_comment_type(),
get_comment_type_object(), get_comment_types(), comment_type_exists(), the
WP_Comment_Type class, and the registered/unregistered actions and label
filters. Verify the built-in types are registered and cannot be unregistered,
and that the comment_type() template tag renders registered type labels while
preserving built-in output.

See #35214.
@github-actions

github-actions Bot commented Jun 24, 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.

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

Props adamsilverstein.

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


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

Copy link
Copy Markdown

Hi there! 👋

Thank you for your contribution to WordPress! 💖

It looks like this is your first pull request to wordpress-develop. Here are a few things to be aware of that may help you out!

No one monitors this repository for new pull requests. Pull requests must be attached to a Trac ticket to be considered for inclusion in WordPress Core. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description.

Pull requests are never merged on GitHub. The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making.

More information about how GitHub pull requests can be used to contribute to WordPress can be found in the Core Handbook.

Please include automated tests. Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the Automated Testing page in the handbook.

If you have not had a chance, please review the Contribute with Code page in the WordPress Core Handbook.

The Developer Hub also documents the various coding standards that are followed:

Thank you,
The WordPress Project

@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.

…abel.

Omit 'label' from the WP_Comment_Type::set_props() defaults so the property
stays null when not provided. A false default was treated as a supplied value
by _get_custom_object_labels(), which overwrote the default name with false,
leaving labels->name and label as false for any type registered without an
explicit label. This mirrors WP_Post_Type and WP_Taxonomy, which omit 'label'
from their defaults for the same reason.

Add a regression test covering default labels for a label-less comment type.

See #35214.
The default branch of comment_type() echoed a registered type's
singular_name label without escaping. Labels are developer-supplied
(via register_comment_type() or the comment_type_labels_{$type}
filter) and reach the public comment list unescaped, so wrap the
output in esc_html() to match the post type and taxonomy label
contract. Document that labels are stored unescaped and add a
regression test asserting an HTML payload in the label is escaped.
adamsilverstein added a commit to adamsilverstein/wordpress-develop that referenced this pull request Jun 25, 2026
Expose registered comment types through a read-only `/wp/v2/comment-types`
controller, mirroring the post types controller (`/wp/v2/types`). This lets
REST clients discover the registered types and their labels, which the block
editor's inline-commenting work needs in order to add and query comments by
type.

Add a `show_in_rest` argument to `WP_Comment_Type` (defaulting to the value of
`public`, as `show_ui` does) to gate which types the endpoint exposes. The
built-in `comment`, `pingback`, and `trackback` types are public and therefore
visible; the internal `note` type is not.

Builds on the registration API in WordPress#12311.

See #35214.
Add reset_comment_types() to WP_UnitTestCase_Base, mirroring
reset_post_types()/reset_taxonomies(), plus an _unregister_comment_type()
helper. This removes the need for hand-rolled tear_down() cleanup loops
in individual test classes, which never ran when an assertion failed
first, and heals any test that mutates the built-in types.
…stration.

Nothing in the comment type API consumes 'show_ui' yet; the admin list
table work that would read it is still design-gated. Removing an
argument after release is impossible while adding one later is trivial,
so defer it to the PR that introduces the admin UI.
register_comment_type() previously overwrote an existing registration
silently, matching register_post_type(). For built-in comment types that
would let a plugin strip flags core relies on (for example rendering and
query behavior), so reject those with _doing_it_wrong() and a WP_Error
instead. Core's own repeated registrations on 'init' and 'change_locale'
pass '_builtin' and remain allowed, and unregister_comment_type()
already guards built-ins the same way.

Custom (non-built-in) types keep silent-overwrite semantics, now pinned
by a test alongside register -> unregister -> register.
- Replace the inaccurate 'upgrade routine' rationale on
  register_comment_type() with the real reason registrations should not
  be hooked before 'init' (translations must be loaded).
- Rewrite the 'public' and 'internal' docs to describe what core
  actually does with them today instead of promising behavior that is
  not implemented; note why 'public' defaults to true.
- Move the misplaced escaping paragraph in the
  comment_type_labels_{$comment_type} filter docblock above the tags so
  the code reference parser keeps it.
- Add the missing @SInCE 7.1.0 changelog entry to comment_type() for
  the new singular-label fallback.
- Type get_comment_types() $args as array: a string would fatal in
  wp_filter_object_list() on PHP 8.
- Note the deliberate i18n string reuse in create_initial_comment_types()
  and include comment types in the _get_custom_object_labels() summary.
- Fix label docblock nits and @PARAM alignment.
_get_custom_object_labels() unconditionally derives 'name_admin_bar'
and spawns 'all_items'/'archives' when a 'menu_name' is provided, all
of which are meaningless for comment types. Strip them from the labels
object unless explicitly provided at registration.

Also expand test coverage for the registration API: name sanitization
and the 20-character boundary, get_comment_types() operators and
output keying, the label fallback chain, filter backfill of required
labels, action arguments, non-scalar lookups, idempotent built-in
registration, template output for legacy empty and built-in 'note'
types, and make the reset_default_labels() test able to fail by
poisoning the static cache. Add per-method @Covers tags.
reset_comment_types() unregistered only the types reporting _builtin false, so a
test that registers a custom type with '_builtin' set left it in the registry for
every test that followed - which then hit the built-in re-registration guard for
a name it had never registered. Registering a comment type creates no hooks,
rewrite rules, or meta boxes, so emptying the registry and re-running
create_initial_comment_types() is both complete cleanup and simpler.

See #35214.
…ype.

The hierarchical property exists only so the shared label helper has a slot to
resolve against, and comment types deliberately leave that slot null. set_props()
copies every provided argument onto the object, though, so a plugin carrying
`'hierarchical' => true` over from register_post_type() flipped the property and
resolved name, singular_name, menu_name, and label to null. Nothing errored - the
type was silently unlabeled, and comment_type()'s isset() check hid it.

Force the property false alongside the other derived values.

See #35214.
…ntract.

WP_Comment_Query reads 'all', 'comments', and 'pings' as query tokens rather than
as literal comment_type values, so a type registered under one of those names
could never be queried for on its own. Reject them at registration instead of
letting a plugin discover it later.

The rest is contract that was implied but never written down, and the follow-up
PRs in this stack each add another argument, so it is worth fixing the shape now
rather than accumulating a register_post_type()-style cascade: each argument
drives exactly one layer of behavior and never implies another, `internal` is the
one that drives default query and count exclusion, and `show_in_rest` defaulting
from `public` is the single cascade planned. Also document that '_builtin' is for
core's own use, that unregistered types behave exactly as they did before this
API existed, and that the registry is per-process rather than per-site.

Pin the edges with tests: '_builtin' passed by a plugin both bypasses the
re-registration guard and locks a custom type, built-in labels rebuild on a
locale change, the accessors cope with the registry global not existing yet, and
a registration survives switch_to_blog().

See #35214.
…ject.

_get_custom_object_labels() writes every label it derives back onto the
object it receives, including the post-type-only labels this function
strips from its return value. Calling get_comment_type_labels() on an
already registered comment type therefore polluted the registered
object's labels with name_admin_bar, all_items, and archives, and a
second call returned them, breaking the documented label set. The
registration path masked this because set_props() reassigns labels
from the return value immediately.

Pass a copy of the object into the helper so the registered object is
never touched, and pin the invariant with a repeated-call test.

See #35214.
The internal argument's docs stated that internal types are excluded
from comment queries and counts through the
default_excluded_comment_types filter, but that filter is proposed
separately (see #65537) and does not exist in this change. A plugin
author following the argument contract would expect an exclusion that
never happens, and the {@see} tag pointed hook docs at a nonexistent
hook.

Describe the flag the way public is already described: what it is
meant to drive, with an explicit note that core does not act on it
yet. The wording can be tightened back up when the exclusion filter
lands.

See #35214.
Unlike _unregister_post_type() and _unregister_taxonomy(), which tests
call to tear down objects registered in wpSetUpBeforeClass(), nothing
in the suite calls this helper: reset_comment_types() already drops
the whole registry before every test, so per-type teardown has no use
case yet. Remove it rather than shipping dead code; it can return if
a real caller appears.

See #35214.
The branch was written while trunk was 7.1-alpha; trunk is now
7.2-alpha, so the API's @SInCE tags and _doing_it_wrong() version
strings would have credited the wrong release and shown plugin
authors a wrong version at runtime. Pre-existing 7.1.0 stamps for
features that did ship in 7.1 are untouched.

See #35214.
@github-actions

This comment has been minimized.

adamsilverstein and others added 6 commits November 8, 2026 09:06
PHPStan rejected assigning get_comment_type_labels() to the stdClass
`labels` property, because documenting the return as a bare `object`
hash yields a structural shape. Documenting it as `stdClass`, the way
get_post_type_labels() does, keeps the shape while matching what the
function builds.

Level 10 analysis also flagged the remaining new code: the args arrays
now declare their key types, the operator is limited to the values
wp_filter_object_list() accepts, void methods say so, and
unregister_comment_type() checks the looked-up object directly instead
of asking comment_type_exists() first. comment_type() now checks that a
filtered singular label is a string before escaping it, since a filter
can replace the label with anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vwYjUcjKij9YncprcpAvX
Level 10 PHPStan analysis could not follow these tests: lookups that may
return null were dereferenced directly, and one assertion was reported
as always false because the analyser remembered an earlier null result
for the same call. A small helper now asserts the type is registered
before returning it, test methods declare `void`, and the data provider,
callbacks and output helper carry native types. The one deliberate
non-scalar lookup keeps an ignore with its reason, matching other core
tests that pass invalid input on purpose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vwYjUcjKij9YncprcpAvX
Trunk now ships a `reaction` comment type for notes reactions and treats
it like `note` throughout core (excluded from default queries, counts,
and feeds). Without a registration, a plugin could claim the `reaction`
key with its own labels and flags, or unregister it.

Register it alongside `note` with the same internal, non-public,
built-in arguments and its own labels. The built-in type count used by
the idempotency test goes from four to five.

See #35214.
Several doc blocks and inline comments added for the comment type API
narrated design history and repeated the same rationale in more than one
place. Trim them to what a caller or maintainer needs and fix an action
doc that called itself a filter.

See #35214.
Trunk is in the 7.2 beta, so these additions can no longer ship in 7.2.0.
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.

1 participant