Skip to content

Comments: Allow comment types to register a display callback - #53

Open
adamsilverstein wants to merge 17 commits into
feature/register-comment-typefrom
feature/comment-type-render-callback
Open

adamsilverstein wants to merge 17 commits into
feature/register-comment-typefrom
feature/comment-type-render-callback

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jun 24, 2026 •

Copy link
Copy Markdown
Owner

Stack: Custom Comment Types (#35214)

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

WordPress#12311 and WordPress#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 took another pass at explaining this one:

What

Adds a render_callback argument to register_comment_type() so a custom comment type can control how its comments display in wp_list_comments(), while the usual overrides still win.

register_comment_type( 'review', array(
	'render_callback' => function ( $comment, $args, $depth ) {
		$tag = ( 'div' === $args['style'] ) ? 'div' : 'li';
		printf( '<%s class="review">%s', $tag, esc_html( $comment->comment_content ) );
	},
) );

Why

This handles dshanske's request on the tracking ticket for "a way to register a callback to handle display that can still be overridden by the usual methods." See https://core.trac.wordpress.org/ticket/35214#comment:18

How

  • WP_Comment_Type gets a $render_callback property (default null), set through set_props().
  • Walker_Comment::start_el() calls the type's callback with the same arguments as the wp_list_comments() callback (comment, args, depth). An explicit callback passed to wp_list_comments() still takes precedence, and types without a callback render exactly as before.
  • The contract matches the wp_list_comments() callback. Echo only the opening <li> (or <div> under the div style) and Walker_Comment::end_el() closes it after any children. Output is echoed, not returned (unlike block render_callback), and isn't escaped for you.
  • Built-in types have no callback, but one can be added through the register_comment_type_args filter. Note that on the comment type this also replaces the walker's link stripping for pending comments.
  • Classic themes only. The core/comment-template block doesn't go through Walker_Comment.

Testing

  • vendor/bin/phpunit --group comment
  • vendor/bin/phpunit --filter 'Tests_Comment_Walker|Tests_Comment_WpCommentType'
  • Manual check in a classic theme (eg. Twenty Twenty-One): register a review type with a callback like the one above, add a comment with comment_type set to review, and confirm the post's comment list uses your markup and replies nest inside it. Then pass 'callback' to wp_list_comments() in the theme and confirm it overrides the type callback.

Out of scope / follow-ups

  • Generalizing the hard-coded pingback/trackback checks in Walker_Comment and separate_comments() is handled by the ping grouping PR stacked on this one.
  • Block theme support (core/comment-template) would need its own approach.

See #35214.

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

AI Use

This code and description came out of a conversation with 🤖 Claude Code. I will review and test.

Add a `render_callback` argument to `register_comment_type()`, stored on
`WP_Comment_Type`. When a comment's registered type defines one, `Walker_Comment`
uses it to render that comment, receiving the same arguments as the `callback`
argument of wp_list_comments() (the comment, the arguments, and the depth).

This gives custom comment types control over their own display, as raised in
the tracking ticket, without each type having to filter the walker. An explicit
`callback` passed to wp_list_comments() still takes precedence, and built-in
types set no callback, so there is no change to existing output.

See #35214.
Cover the `render_callback` argument: that a registered type's callback renders
its comments through `Walker_Comment`, that an explicit wp_list_comments()
`callback` takes precedence, that a type without a callback renders normally,
and that the property defaults to null and stores a provided callable.

See #35214.
@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3e58518a-7a61-4c38-bcff-ab5122765578

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
Finishing Touches

🧪 Generate unit tests (beta)

  • Create PR with unit tests
  • Commit unit tests in branch feature/comment-type-render-callback
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

…ard.

Add a test asserting the render_callback receives the comment, the
arguments array, and the depth (the documented wp_list_comments()
callback contract, previously only the comment was exercised), and a
test that a non-callable render_callback is ignored via is_callable()
and the comment renders normally.

See #35214.
Walker_Comment prints the render_callback output verbatim, matching the
existing wp_list_comments() `callback` contract. Note in both the
WP_Comment_Type::$render_callback property and the register_comment_type()
parameter docs that the callback is responsible for escaping its output.
…e-render-callback

# Conflicts:
#	src/wp-includes/class-wp-comment-type.php
#	src/wp-includes/comment.php
…th tests.

- State the full precedence chain (explicit wp_list_comments()
  'callback', then 'render_callback', then default markup), the
  open-tag contract (output only the element opening; end_el() or
  'end-callback' closes it after children), and the classic-theme-only
  scope (block themes never invoke Walker_Comment) in both the property
  and args docblocks.
- Add the missing @SInCE 7.1.0 changelog entry to
  Walker_Comment::start_el().
- Fix the test callbacks, which echoed closed <li> elements: end_el()
  appends its own close, so the previous assertions passed while the
  markup contained stray closing tags. Assert the full element shape,
  pin end_el()'s close, and cover a threaded child rendering inside its
  parent's callback-opened element plus legacy empty-string types.
- Document the render_comments() helper's $args parameter and drop
  cleanup-only unregister calls now handled by the test framework.
…e-render-callback

# Conflicts:
#	src/wp-includes/comment.php
…tract.

Three parts of the contract were left for a developer to infer:

The callback opens the element that `Walker_Comment::end_el()` closes, and
`end_el()` emits `</div>` or `</li>` depending on the `style` argument. The docs
said "an unclosed `<li>` by default", which is true but reads as the whole
story - a callback that always opens an `<li>` produces mismatched markup under
`style => 'div'`. Say that the callback has to branch on `$args['style']`, the
way the built-in `comment()` does.

The name deliberately echoes `register_block_type()`, whose `render_callback`
returns a string. This one is captured with an output buffer, so a return value
is silently discarded. Say "must echo" rather than "output".

Built-in types cannot be re-registered, but `set_props()` applies the
registration args filters to them too, so `register_comment_type_args` can set a
callback on 'comment', 'pingback', or 'trackback'. That is worth keeping - it
grants no more than the `callback` argument of wp_list_comments() always has -
so document it as the intended override path, along with the one consequence
that is easy to miss: a callback on the 'comment' type replaces the walker's
link stripping for pending comments.
…aths.

The suite covered the default `<li>` style only, so nothing would catch a change
that broke the pairing between a callback's opening tag and `end_el()`'s closing
one under `style => 'div'`.

Also pin the filter-based override of a built-in type. That path falls out of
where `set_props()` applies the registration args filters, and is now documented
as intended, so it needs a test to keep it from being tightened away as an
oversight.
…e-render-callback

# Conflicts:
#	src/wp-includes/comment.php
The branch was written while trunk was 7.1-alpha; trunk is now
7.2-alpha, so the render_callback property and the Walker_Comment
behavior-change note carried @SInCE tags for a release that never
contained them - the only two stragglers after the base branch
re-stamped itself.

Also pin the one documented clause of the callback contract that had
no test: a returned string is discarded, output must be echoed. A
refactor of the ob_start()/call_user_func() dispatch could otherwise
start honoring return values with the suite staying green.

See #35214.
The property, argument, and walker comments repeated the same contract
several times and read as design notes. Trimming them to what a caller
needs (arguments, precedence, open-tag and echo rules, escaping, the
'comment' type caveat) keeps the docs scannable and accurate.

See #35214.
New tests should declare return types, and the render_comments() helper
casts wp_list_comments() output so its string return type holds. Test
docblocks are trimmed to state what each test asserts.

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