Skip to content
This repository was archived by the owner on Feb 5, 2026. It is now read-only.

Commit 309f5a0

Browse files
justlevinegziolo
andauthored
dev: add register_ability_args filter [Proposal] (#74)
* dev: add `wp_ability_args` filter * Update docs/5.hooks.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * docs: fix `has_permissions` overload example * fix: rename/relocate filter and fix docs usage Co-authored-by: Greg Ziółkowski <grzegorz@gziolo.pl> * docs: update remaining references * docs: merge hook docs * docs: code indentation * tests: add PHPUnit tests for `register_ability_args` * Update docs/6.hooks.md Co-authored-by: Greg Ziółkowski <grzegorz@gziolo.pl> * tests: remove prior filters in _every_ setup/teardown --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Greg Ziółkowski <grzegorz@gziolo.pl> Co-authored-by: justlevine <justlevine@git.wordpress.org> Co-authored-by: gziolo <gziolo@git.wordpress.org>
1 parent 4beab25 commit 309f5a0

3 files changed

Lines changed: 229 additions & 34 deletions

File tree

docs/6.hooks.md

Lines changed: 100 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,134 @@
11
# 6. Hooks
22

3-
The Abilities API provides [WordPress Action Hooks](https://developer.wordpress.org/apis/hooks/) that allow developers to monitor and respond to ability execution events.
3+
The Abilities API provides [WordPress Action and Filter Hooks](https://developer.wordpress.org/apis/hooks/) that allow developers to monitor and respond to ability execution events.
44

5-
## Available Actions
5+
## Quick Links
6+
7+
- [Actions](#actions)
8+
- [`before_execute_ability`](#before_execute_ability)
9+
- [`after_execute_ability`](#after_execute_ability)
10+
- [Filters](#filters)
11+
- [`register_ability_args`](#register_ability_args)
12+
13+
## Actions
614

715
### `before_execute_ability`
816

917
Fires immediately before an ability gets executed, after permission checks have passed but before the execution callback is called.
1018

1119
```php
12-
/**
13-
* Fires before an ability gets executed.
14-
*
15-
* @since n.e.x.t
16-
*
17-
* @param string $ability_name The name of the ability.
18-
* @param mixed $input The input data for the ability.
19-
*/
2020
do_action( 'before_execute_ability', $ability_name, $input );
2121
```
2222

23-
**Parameters:**
23+
#### Parameters
2424

2525
- `$ability_name` (`string`): The namespaced name of the ability being executed (e.g., `my-plugin/get-posts`).
2626
- `$input` (`mixed`): The input data passed to the ability.
2727

28+
#### Usage Example
29+
30+
```php
31+
/**
32+
* Log each ability execution attempt.
33+
* @param string $ability_name The name of the ability being executed.
34+
* @param mixed $input The input data passed to the ability.
35+
*/
36+
function log_ability_execution( string $ability_name, $input ) {
37+
error_log( 'About to execute ability: ' . $ability_name );
38+
if ( $input !== null ) {
39+
error_log( 'Input: ' . wp_json_encode( $input ) );
40+
}
41+
}
42+
add_action( 'before_execute_ability', 'log_ability_execution', 10, 2 );
43+
```
44+
2845
### `after_execute_ability`
2946

3047
Fires immediately after an ability has finished executing successfully, after output validation has passed.
3148

3249
```php
33-
/**
34-
* Fires immediately after an ability finished executing.
35-
*
36-
* @since n.e.x.t
37-
*
38-
* @param string $ability_name The name of the ability.
39-
* @param mixed $input The input data for the ability.
40-
* @param mixed $result The result of the ability execution.
41-
*/
42-
do_action( 'after_execute_ability', $ability_name, $input, $result );
50+
do_action( 'after_execute_ability', string $ability_name, $input, $result );
4351
```
4452

45-
**Parameters:**
53+
#### Parameters
4654

4755
- `$ability_name` (`string`): The namespaced name of the ability that was executed.
4856
- `$input` (`mixed`): The input data that was passed to the ability.
4957
- `$result` (`mixed`): The validated result returned by the ability's execution callback.
5058

51-
## Usage Examples
59+
#### Usage Example
5260

53-
**Basic Logging**
61+
```php
62+
/**
63+
* Log the result of each ability execution.
64+
*
65+
* @param string $ability_name The name of the executed ability.
66+
* @param mixed $input The input data passed to the ability.
67+
* @param mixed $result The result returned by the ability.
68+
*/
69+
function log_ability_result( string $ability_name, $input, $result ) {
70+
error_log( 'Completed ability: ' . $ability_name );
71+
error_log( 'Result: ' . wp_json_encode( $result ) );
72+
}
73+
add_action( 'after_execute_ability', 'log_ability_result', 10, 3 );
74+
```
75+
76+
## Filters
77+
78+
### `register_ability_args`
79+
80+
Allows modification of an Ability's args before they are validated and used to instantiate the Ability.
5481

5582
```php
56-
// Log all ability executions.
57-
add_action( 'before_execute_ability', function( $ability_name, $input ) {
58-
error_log( 'Executing ability: ' . $ability_name );
59-
if ( $input !== null ) {
60-
error_log( 'Input: ' . wp_json_encode( $input ) );
83+
$args = apply_filters( 'register_ability_args', array $args, string $ability_name );
84+
```
85+
86+
#### Parameters
87+
88+
- `$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.
89+
- `$ability_name` (`string`): The namespaced name of the ability being registered (e.g., `my-plugin/get-posts`).
90+
91+
#### Usage Example
92+
93+
```php
94+
/**
95+
* Modify ability args before validation.
96+
*
97+
* @param array<string,mixed> $args The arguments used to instantiate the ability.
98+
* @param string $ability_name The name of the ability, with its namespace.
99+
*
100+
* @return array<string,mixed> The modified ability arguments.
101+
*/
102+
function my_modify_ability_args( array $args, string $ability_name ): array {
103+
// Check if the ability name matches what you're looking for.
104+
if ( 'my-namespace/my-ability' !== $ability_name ) {
105+
return $args;
61106
}
62-
}, 10, 2 );
63107

64-
add_action( 'after_execute_ability', function( $ability_name, $input, $result ) {
65-
error_log( 'Completed ability: ' . $ability_name );
66-
error_log( 'Result: ' . wp_json_encode( $result ) );
67-
}, 10, 3 );
108+
// Modify the args as needed.
109+
$args['label'] = __('My Custom Ability Label');
110+
111+
// You can use the old args to build new ones.
112+
$args['description'] = sprintf(
113+
/* translators: 1: Ability name 2: Previous description */
114+
__('This is a custom description for the ability %s. Previously the description was %s', 'text-domain'),
115+
$ability_name,
116+
$args['description'] ?? 'N/A'
117+
);
118+
119+
// Even if they're callbacks.
120+
$args['permission_callback' ] = static function ( $input = null ) use ( $args, $ability_name ) {
121+
$previous_check = is_callable( $args['permission_callback'] ) ? $args['permission_callback']( $input ) : true;
122+
123+
// If we already failed, no need for stricter checks.
124+
if ( ! $previous_check || is_wp_error( $previous_check ) ) {
125+
return $previous_check;
126+
}
127+
128+
return current_user_can( 'my_custom_ability_cap', $ability_name );
129+
}
130+
131+
return $args;
132+
}
133+
add_filter( 'register_ability_args', 'my_modify_ability_args', 10, 2 );
68134
```

includes/abilities-api/class-wp-abilities-registry.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ public function register( string $name, array $args ): ?WP_Ability {
8484
return null;
8585
}
8686

87+
/**
88+
* Filters the ability arguments before they are validated and used to instantiate the ability.
89+
*
90+
* @since n.e.x.t
91+
*
92+
* @param array<string,mixed> $args The arguments used to instantiate the ability.
93+
* @param string $name The name of the ability, with its namespace.
94+
*/
95+
$args = apply_filters( 'register_ability_args', $args, $name );
96+
8797
// The class is only used to instantiate the ability, and is not a property of the ability itself.
8898
if ( isset( $args['ability_class'] ) && ! is_a( $args['ability_class'], WP_Ability::class, true ) ) {
8999
_doing_it_wrong(
@@ -93,6 +103,8 @@ public function register( string $name, array $args ): ?WP_Ability {
93103
);
94104
return null;
95105
}
106+
107+
/** @var class-string<\WP_Ability> */
96108
$ability_class = $args['ability_class'] ?? WP_Ability::class;
97109
unset( $args['ability_class'] );
98110

tests/unit/abilities-api/wpAbilitiesRegistry.php

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public function set_up(): void {
2727

2828
$this->registry = new WP_Abilities_Registry();
2929

30+
remove_all_filters( 'register_ability_args' );
31+
3032
self::$test_ability_args = array(
3133
'label' => 'Add numbers',
3234
'description' => 'Calculates the result of adding two numbers.',
@@ -69,6 +71,8 @@ public function set_up(): void {
6971
public function tear_down(): void {
7072
$this->registry = null;
7173

74+
remove_all_filters( 'register_ability_args' );
75+
7276
parent::tear_down();
7377
}
7478

@@ -427,4 +431,117 @@ public function test_wp_ability_invalid_properties_throws_exception() {
427431
)
428432
);
429433
}
434+
435+
/**
436+
* Test register_ability_args filter modifies the args before ability instantiation.
437+
*/
438+
public function test_register_ability_args_filter_modifies_args() {
439+
$was_filter_callback_fired = false;
440+
441+
// Define the filter.
442+
add_filter(
443+
'register_ability_args',
444+
static function ( $args ) use ( &$was_filter_callback_fired ) {
445+
$args['label'] = 'Modified label';
446+
$original_execute_callback = $args['execute_callback'];
447+
$args['execute_callback'] = static function ( array $input ) use ( &$was_filter_callback_fired, $original_execute_callback ) {
448+
$was_filter_callback_fired = true;
449+
return $original_execute_callback( $input );
450+
};
451+
452+
return $args;
453+
},
454+
10
455+
);
456+
457+
// Register the ability.
458+
$ability = $this->registry->register( self::$test_ability_name, self::$test_ability_args );
459+
460+
// Check the label was modified by the filter.
461+
$this->assertSame( 'Modified label', $ability->get_label() );
462+
463+
// Call the execute callback.
464+
$result = $ability->execute(
465+
array(
466+
'a' => 1,
467+
'b' => 2,
468+
)
469+
);
470+
471+
$this->assertTrue( $was_filter_callback_fired, 'The execute callback defined in the filter was not fired.' );
472+
$this->assertSame( 3, $result, 'The original execute callback did not return the expected result.' );
473+
}
474+
475+
/**
476+
* Test register_ability_args filter can block ability registration by returning invalid args.
477+
*
478+
* @expectedIncorrectUsage WP_Abilities_Registry::register
479+
*/
480+
public function test_register_ability_args_filter_blocks_registration() {
481+
// Define the filter.
482+
add_filter(
483+
'register_ability_args',
484+
static function ( $args ) {
485+
// Remove the label to make the args invalid.
486+
unset( $args['label'] );
487+
return $args;
488+
},
489+
10
490+
);
491+
492+
// Register the ability.
493+
$ability = $this->registry->register( self::$test_ability_name, self::$test_ability_args );
494+
495+
// Check the ability was not registered.
496+
$this->assertNull( $ability, 'The ability was registered even though the args were made invalid by the filter.' );
497+
}
498+
499+
/**
500+
* Test register_ability_args filter can block an invalid ability class from being used.
501+
* @expectedIncorrectUsage WP_Abilities_Registry::register
502+
*/
503+
public function test_register_ability_args_filter_blocks_invalid_ability_class() {
504+
// Define the filter.
505+
add_filter(
506+
'register_ability_args',
507+
static function ( $args ) {
508+
// Set an invalid ability class.
509+
$args['ability_class'] = 'NonExistentClass';
510+
return $args;
511+
},
512+
10
513+
);
514+
// Register the ability.
515+
$ability = $this->registry->register( self::$test_ability_name, self::$test_ability_args );
516+
517+
// Check the ability was not registered.
518+
$this->assertNull( $ability, 'The ability was registered even though the ability class was made invalid by the filter.' );
519+
}
520+
521+
/**
522+
* Tests register_ability_args filter is only applied to the specific ability being registered.
523+
*/
524+
public function test_register_ability_args_filter_only_applies_to_specific_ability() {
525+
add_filter(
526+
'register_ability_args',
527+
static function ( $args, $name ) {
528+
if ( self::$test_ability_name !== $name ) {
529+
// Do not modify args for other abilities.
530+
return $args;
531+
}
532+
533+
$args['label'] = 'Modified label for specific ability';
534+
return $args;
535+
},
536+
10,
537+
2
538+
);
539+
540+
// Register the first ability, which the filter should modify.
541+
$filtered_ability = $this->registry->register( self::$test_ability_name, self::$test_ability_args );
542+
$this->assertSame( 'Modified label for specific ability', $filtered_ability->get_label() );
543+
544+
$unfiltered_ability = $this->registry->register( 'test/another-ability', self::$test_ability_args );
545+
$this->assertNotSame( $filtered_ability->get_label(), $unfiltered_ability->get_label(), 'The filter incorrectly modified the args for an ability it should not have.' );
546+
}
430547
}

0 commit comments

Comments
 (0)