Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs/5.hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Using Action and Filter Hooks

The Abilities API provides action and filter hooks that allow developers to customize and extend its functionality. Below are the available hooks:

## Quick Links

- [Actions](#actions)
- [Filters](#filters)
- [`wp_ability_args`](#wp_ability_args)

## Actions

There are currently **no Action hooks** provided by this version of the Abilities API.

## Filters

### `wp_ability_args`

> [!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.
Comment on lines +19 to +21
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.

Allows modification of an Ability's args before they are validated and used to instantiate the Ability.

```php
apply_filters( 'wp_ability_args', array $args, string $ability_name );
```

#### Parameters

- `$args` (`array<string,mixed>`): The arguments used to instantiate the ability. See [wp_register_ability()](./3.registering-abilities.md#wp_register_ability) for the full list of args.
- `$ability_name` (`string`): The name of the ability, with its namespace.

#### Example

```php
/**
* Modify ability args before validation.
* @param array<string,mixed> $args The arguments used to instantiate the ability.
* @param string $ability_name The name of the ability, with its namespace.
* @return array<string,mixed> The modified ability arguments.
*/
function my_modify_ability_args( array $args, string $ability_name ): array {
// Check if the ability name matches what you're looking for.
if ( 'my-namespace/my-ability' !== $ability_name ) {
return $args;
}

// Modify the args as needed.
$args['label'] = __('My Custom Ability Label');

// You can use the old args to build new ones.
$args['description'] = sprintf(
/* translators: 1: Ability name 2: Previous description */
__('This is a custom description for the ability %s. Previously the description was %s', 'text-domain'),
$ability_name,
$args['description'] ?? 'N/A'
);

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

$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', $ability_name );
}

return $args;
}
add_filter( 'wp_ability_args', 'my_modify_ability_args', 10, 2 );
```
12 changes: 12 additions & 0 deletions includes/abilities-api/class-wp-abilities-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ final class WP_Abilities_Registry {
* } $args
*/
public function register( string $name, array $args ): ?WP_Ability {
/**
* Filters the ability arguments before they are validated.
*
* @since n.e.x.t
*
* @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 👍🏻


if ( ! preg_match( '/^[a-z0-9-]+\/[a-z0-9-]+$/', $name ) ) {
_doing_it_wrong(
__METHOD__,
Expand Down Expand Up @@ -94,6 +104,8 @@ public function register( string $name, array $args ): ?WP_Ability {
);
return null;
}

/** @var class-string<\WP_Ability> */
$ability_class = $args['ability_class'] ?? WP_Ability::class;
unset( $args['ability_class'] );

Expand Down