Skip to content

Conversation

justlevine
Copy link
Contributor

@justlevine justlevine commented Sep 13, 2025

What

This PR exposes the ability_args wp_ability_args filter as a way to centrally extend and change the behavior of registered abilities.

Why

How

  • wp_ability_args is used before the merge to core, but it should be changed to abilities_api as part of 6.9 IMO.
    Once merged into core, we can just add_filter( 'ability_args', function ( ... ) { return apply_filters_deprecated( 'wp_ability_args' ); to allow anyone who builds something between now and December time to transition.

@justlevine justlevine requested a review from Copilot September 13, 2025 17:03
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces the wp_ability_args filter to the Abilities API, allowing developers to modify ability arguments before validation and instantiation. This provides a centralized mechanism for extending and customizing ability behavior.

  • Adds a filter hook in the registry's register method to modify ability arguments
  • Includes comprehensive documentation with usage examples
  • Uses wp_ prefix to avoid naming collisions before core integration

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
includes/abilities-api/class-wp-abilities-registry.php Adds the wp_ability_args filter in the register method before validation
docs/5.hooks.md Documents the new filter with detailed usage examples and parameters

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

docs/5.hooks.md Outdated
Comment on lines 61 to 70
$args['has_permissions' ] = static function ( $input = null ) use ( $args ) {
$previous_check = is_callable( $args['has_permissions'] ) ? $args['has_permissions']( $input ) : true;

// If we already failed, no need for stricter checks.
if ( ! $previous_check || is_wp_error( $previous_check ) ) {
return $previous_check;
}

return current_user_can( 'my_custom_ability_cap', $args['name'] )
}
Copy link
Preview

Copilot AI Sep 13, 2025

Choose a reason for hiding this comment

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

This creates a circular reference issue. The closure captures $args by reference, but then tries to access $args['has_permissions'] which would be the closure itself, causing infinite recursion. The original callback should be stored in a separate variable before reassigning.

Suggested change
$args['has_permissions' ] = static function ( $input = null ) use ( $args ) {
$previous_check = is_callable( $args['has_permissions'] ) ? $args['has_permissions']( $input ) : true;
// If we already failed, no need for stricter checks.
if ( ! $previous_check || is_wp_error( $previous_check ) ) {
return $previous_check;
}
return current_user_can( 'my_custom_ability_cap', $args['name'] )
}
$previous_has_permissions = $args['has_permissions'] ?? null;
$args['has_permissions'] = static function ( $input = null ) use ( $args, $previous_has_permissions ) {
$previous_check = is_callable( $previous_has_permissions ) ? $previous_has_permissions( $input ) : true;
// If we already failed, no need for stricter checks.
if ( ! $previous_check || is_wp_error( $previous_check ) ) {
return $previous_check;
}
return current_user_can( 'my_custom_ability_cap', $args['name'] );
};

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I dont think this is true 🤔 we're only calling inside the function, not reassigning.

Copy link

codecov bot commented Sep 13, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.36%. Comparing base (59d8ec6) to head (dc10954).

Additional details and impacted files
@@             Coverage Diff              @@
##              trunk      #74      +/-   ##
============================================
+ Coverage     83.33%   83.36%   +0.03%     
  Complexity       96       96              
============================================
  Files             8        8              
  Lines           516      517       +1     
============================================
+ Hits            430      431       +1     
  Misses           86       86              
Flag Coverage Δ
unit 83.36% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@justlevine justlevine requested a review from gziolo September 13, 2025 17:16
@justlevine justlevine marked this pull request as ready for review September 13, 2025 17:17
Copy link

github-actions bot commented Sep 13, 2025

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.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: justlevine <[email protected]>
Co-authored-by: gziolo <[email protected]>

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

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

I left some minor feedback. Overall, I'm very much in favor of the proposed extensibility option.

* @param array<string,mixed> $args The arguments used to instantiate the ability.
* @param string $name The name of the ability, with its namespace.
*/
$args = apply_filters( 'wp_ability_args', $args, $name );
Copy link
Member

Choose a reason for hiding this comment

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

Minor note, this could get moved after all checks against $name to ensure it doesn't get processed on abilities that have an incorrect name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My intent here is intentionally the opposite, since it allows changes/recoverability of the $name while still ensuring it passes the necessary validation for downstream checks.

IMO (pseudocode):

add_filter( 'register_ability_args',
  // Or whatever prefixes/missing-namespaces are enforced by validation.
  static fn ( &$args, $original_name ) => $args['name'] = sprintf( '!$#@#_%s', str_replace( '/', '_', $original_name ),
10, 2 );

should not be allowed since it means adapters/downstream cant reliably depend on the namespace/shape for their needs.

Meanwhile, I'm believe that e.g.

add_filter( 'register_ability_args',
  static function( $args,) {
    if ( str_starts_with( 'my_legacy_namespace/', $args['name' ) ) {
      $args['name'] = str_replace( 'my_legacy_namespace/', 'my_new_namespace/', $args['name'] );
    }
  }
);

should still be flexible enough for any downstream/back-compat considerations without making the expected shape of the Ability API unreliable, but I'm happy to evaluate other use cases (the only one I can think of is if a user intentionally registers a namespace wrong initially, but the immediate error they face should prevent that from ever shipping and thus preempting the need for a downstream filter)

(PS: moving this to after the checks after a 6.9 merge is a nonbreaking change. moving it up to enforce name validation

Copy link
Member

Choose a reason for hiding this comment

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

$args is not used to read the name. It's a separate variable $name which can't be modified by the hook.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed 🤦 too much task switching that even with the function in front of me I got our $name handling confused with $ability_class. Sorry for wasting your time 😬

I'll move it down to after the ->is_registered() check. When that and the other comments are handled I'll ping you for re-review 🙇

Copy link
Member

Choose a reason for hiding this comment

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

No worries, it's why additional eyes are helpful to the implementation from a different perspective 👍🏻

Comment on lines +19 to +21
> [!IMPORTANT]
> This filter is prefixed with `wp_` to avoid potential naming collisions.
> Once merged into WordPress core, the prefix will likely be removed to preserve backward-compatibility.
Copy link
Member

Choose a reason for hiding this comment

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

I would go with the final name from the start to avoid the hassle. register_ability_args should be distinct enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

register_ability_args is better than ability_args I had in mind too as it aligns with register_{post_type|taxonomy} args.

Copy link
Member

Choose a reason for hiding this comment

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

I'm closely familiar with register_block_type_args, which was likely inspired by these you listed.

);

// Even if they're callbacks.
$args['has_permissions' ] = static function ( $input = null ) use ( $args, $ability_name ) {
Copy link
Member

Choose a reason for hiding this comment

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

This should be permission_callback rather than has_permissions.

@gziolo gziolo added the [Type] Enhancement New feature or request label Sep 15, 2025
@gziolo
Copy link
Member

gziolo commented Sep 17, 2025

We will also need some basic unit test coverage to illustrate the expected behavior better and prevent future regressions.

I plan to add some tests today to:

We can coordinate how to ensure we avoid more complex merge conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Type] Enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants