Skip to content

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

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

Comments: Allow comment types to register a display callback#53
adamsilverstein wants to merge 9 commits into
feature/register-comment-typefrom
feature/comment-type-render-callback

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Description

Adds a render_callback argument to register_comment_type() so a custom comment type can control how its comments are displayed in comment lists, addressing dshanske's request on the tracking ticket (#35214 comment:18) for "a way to register a callback to handle display that can still be overridden by the usual methods."

What it adds

  • A render_callback registration argument, stored on WP_Comment_Type (default null).
  • Walker_Comment::start_el() dispatches to a comment type's render_callback when one is set, passing the same arguments as the wp_list_comments() callback: the comment, the arguments array, and the depth.
register_comment_type( 'review', array(
    'render_callback' => function ( $comment, $args, $depth ) {
        // Custom markup for a "review" comment.
    },
) );

Precedence / non-breaking

  • An explicit callback passed to wp_list_comments() still takes precedence (handled before the type callback).
  • Built-in types (comment, pingback, trackback) set no callback, so their output is unchanged.

Scope / boundary

Stacked on WordPress#12311 (the register_comment_type() API) and targets that branch; it will be retargeted to trunk once WordPress#12311 lands. This is item 3 of the remaining below-the-hood work on #35214.

Item 4 (generalizing the remaining hard-coded pingback/trackback checks in Walker_Comment and separate_comments()) is intentionally not included: those checks encode a ping-vs-comment distinction the registry does not yet model, so generalizing them safely needs a dedicated "ping grouping" concept on the registration object and carries output-regression risk. It is better handled as its own focused PR.

Testing

$ phpunit --group comment
OK (699 tests, 1696 assertions)

New coverage in tests/phpunit/tests/comment/walker.php and wpCommentType.php. PHPCS reports no new warnings on the changed files and PHPStan is clean.

Review updates

  • The docblocks now state the full precedence chain: an explicit wp_list_comments() 'callback' wins, then the type's render_callback, then the default markup.
  • They also document the open-tag contract - a callback outputs only the opening of the element, and Walker_Comment::end_el() closes it after any children are rendered, the same contract as wp_list_comments() callbacks - and the classic-theme-only scope: block themes render comments through the core/comment-template block and never invoke Walker_Comment.
  • Test fix: the original test callbacks echoed closed <li> elements, which produced stray closing tags the substring assertions missed. The tests now emit unclosed <li> elements and assert the full element shape including end_el()'s close, plus a threaded-child test pinning the contract end to end.

Following a second review pass over the stack, three more parts of the contract that were left to inference:

  • The callback has to branch on $args['style']. end_el() emits </div> under style => 'div' and </li> otherwise. "An unclosed <li> by default" was accurate but read as the whole story, and a callback that always opens an <li> produces mismatched markup. Documented, and pinned with a test under the div style - the suite previously only covered the default.
  • The callback must echo. The name deliberately echoes register_block_type(), whose render_callback returns a string. This one is captured with an output buffer, so a returned string is silently discarded. Now says so, with the contrast called out.
  • Built-ins can acquire a callback through the args filter, and that is intended. set_props() applies register_comment_type_args to _builtin registrations too, so a plugin can set a callback on comment, pingback, or trackback even though re-registration is blocked. Worth keeping - it grants no more than the wp_list_comments() callback argument always has - so it is now documented as the sanctioned override path and covered by a test, rather than left as an accident of where the filter sits. One consequence is called out in the docs: a callback on the comment type also replaces the walker's link stripping for pending comments.

See #35214.

AI Use

Code and description both written 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

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97398cf7-8409-48dc-86c8-2a9f1c62cf67

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

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

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

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