Skip to content

Commit e870f28

Browse files
committed
Feature #64990: Add filtering support to wp_get_abilities() via \$args array
1 parent e1b5463 commit e870f28

2 files changed

Lines changed: 197 additions & 29 deletions

File tree

src/wp-includes/abilities-api.php

Lines changed: 180 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -396,33 +396,201 @@ function wp_get_ability( string $name ): ?WP_Ability {
396396
}
397397

398398
/**
399-
* Retrieves all registered abilities.
399+
* Retrieves registered abilities, optionally filtered by the given arguments.
400400
*
401-
* Returns an array of all ability instances currently registered in the system.
402-
* Use this for discovery, debugging, or building administrative interfaces.
401+
* When called without arguments, returns all registered abilities. When called
402+
* with an $args array, returns only abilities that match every specified condition.
403403
*
404-
* Example:
404+
* Filtering pipeline (executed in order):
405+
*
406+
* 1. Declarative filters (`category`, `namespace`, `meta`) — per-item, AND logic between
407+
* arg types, OR logic within multi-value `category` arrays.
408+
* 2. `match_callback` — per-item, caller-scoped. Return true to include, false to exclude.
409+
* 3. `wp_get_abilities_match` filter — per-item, ecosystem-scoped. Plugins can enforce
410+
* universal inclusion rules regardless of what the caller passed.
411+
* 4. `result_callback` — on the full matched array, caller-scoped. Sort, slice, or reshape.
412+
* 5. `wp_get_abilities_result` filter — on the full array, ecosystem-scoped.
413+
*
414+
* Steps 1–3 run inside a single loop over the registry — no extra iteration.
415+
*
416+
* Examples:
405417
*
406-
* // Prints information about all available abilities.
418+
* // All abilities (unchanged behaviour).
407419
* $abilities = wp_get_abilities();
408-
* foreach ( $abilities as $ability ) {
409-
* echo $ability->get_label() . ': ' . $ability->get_description() . "\n";
410-
* }
420+
*
421+
* // Filter by category.
422+
* $abilities = wp_get_abilities( array( 'category' => 'content' ) );
423+
*
424+
* // Filter by multiple categories (OR logic).
425+
* $abilities = wp_get_abilities( array( 'category' => array( 'content', 'settings' ) ) );
426+
*
427+
* // Filter by namespace.
428+
* $abilities = wp_get_abilities( array( 'namespace' => 'woocommerce' ) );
429+
*
430+
* // Filter by meta.
431+
* $abilities = wp_get_abilities( array( 'meta' => array( 'show_in_rest' => true ) ) );
432+
*
433+
* // Combine filters (AND logic between arg types).
434+
* $abilities = wp_get_abilities( array(
435+
* 'category' => 'content',
436+
* 'namespace' => 'core',
437+
* 'meta' => array( 'show_in_rest' => true ),
438+
* ) );
439+
*
440+
* // Caller-scoped per-item callback.
441+
* $abilities = wp_get_abilities( array(
442+
* 'match_callback' => function ( WP_Ability $ability ) {
443+
* return current_user_can( 'manage_options' );
444+
* },
445+
* ) );
446+
*
447+
* // Caller-scoped result callback (sort + paginate).
448+
* $abilities = wp_get_abilities( array(
449+
* 'result_callback' => function ( array $abilities ) {
450+
* usort( $abilities, fn( $a, $b ) => strcasecmp( $a->get_label(), $b->get_label() ) );
451+
* return array_slice( $abilities, 0, 10 );
452+
* },
453+
* ) );
411454
*
412455
* @since 6.9.0
456+
* @since 7.1.0 Added the `$args` parameter for filtering support.
413457
*
414458
* @see WP_Abilities_Registry::get_all_registered()
415459
*
416-
* @return WP_Ability[] An array of registered WP_Ability instances. Returns an empty
417-
* array if no abilities are registered or if the registry is unavailable.
460+
* @param array $args {
461+
* Optional. Arguments to filter the returned abilities. Default empty array (returns all).
462+
*
463+
* @type string|string[] $category Filter by category slug. A single string or an array of
464+
* slugs — abilities matching any of the given slugs are
465+
* included (OR logic within this arg type).
466+
* @type string $namespace Filter by ability namespace prefix. Pass the namespace
467+
* without a trailing slash, e.g. `'woocommerce'` matches
468+
* `'woocommerce/create-order'`.
469+
* @type array $meta Filter by meta key/value pairs. All conditions must
470+
* match (AND logic). Supports nested arrays for structured
471+
* meta, e.g. `array( 'mcp' => array( 'public' => true ) )`.
472+
* @type callable $match_callback Optional. A callback invoked per ability after declarative
473+
* filters. Receives a WP_Ability instance, returns bool.
474+
* Return true to include, false to exclude.
475+
* @type callable $result_callback Optional. A callback invoked once on the full matched
476+
* array. Receives WP_Ability[], must return WP_Ability[].
477+
* Use for sorting, slicing, or reshaping the result.
478+
* }
479+
* @return WP_Ability[] An array of registered WP_Ability instances matching the given args.
480+
* Returns an empty array if no abilities are registered, the registry is
481+
* unavailable, or no abilities match the given args.
418482
*/
419-
function wp_get_abilities(): array {
483+
function wp_get_abilities( array $args = array() ): array {
420484
$registry = WP_Abilities_Registry::get_instance();
421485
if ( null === $registry ) {
422486
return array();
423487
}
424488

425-
return $registry->get_all_registered();
489+
$abilities = $registry->get_all_registered();
490+
491+
// Bail early when no filtering is requested.
492+
if ( empty( $args ) ) {
493+
return $abilities;
494+
}
495+
496+
$category = isset( $args['category'] ) ? (array) $args['category'] : array();
497+
$namespace = isset( $args['namespace'] ) && is_string( $args['namespace'] ) ? rtrim( $args['namespace'], '/' ) . '/' : '';
498+
$meta = isset( $args['meta'] ) && is_array( $args['meta'] ) ? $args['meta'] : array();
499+
$match_callback = isset( $args['match_callback'] ) && is_callable( $args['match_callback'] ) ? $args['match_callback'] : null;
500+
$result_callback = isset( $args['result_callback'] ) && is_callable( $args['result_callback'] ) ? $args['result_callback'] : null;
501+
502+
$matched = array();
503+
504+
foreach ( $abilities as $ability ) {
505+
// Step 1a: Filter by category (OR logic within the arg).
506+
if ( ! empty( $category ) && ! in_array( $ability->get_category(), $category, true ) ) {
507+
continue;
508+
}
509+
510+
// Step 1b: Filter by namespace prefix.
511+
if ( '' !== $namespace && ! str_starts_with( $ability->get_name(), $namespace ) ) {
512+
continue;
513+
}
514+
515+
// Step 1c: Filter by meta key/value pairs (AND logic, supports nested arrays).
516+
if ( ! empty( $meta ) && ! wp_get_abilities_match_meta( $ability->get_meta(), $meta ) ) {
517+
continue;
518+
}
519+
520+
// Step 2: Caller-scoped per-item callback.
521+
$include = true;
522+
if ( null !== $match_callback ) {
523+
$include = (bool) call_user_func( $match_callback, $ability );
524+
}
525+
526+
/**
527+
* Filters whether an individual ability should be included in the result set.
528+
*
529+
* Fires after the declarative filters and the caller-scoped match_callback.
530+
* Plugins can use this to enforce universal inclusion rules regardless of
531+
* what the caller passed in $args.
532+
*
533+
* @since 7.1.0
534+
*
535+
* @param bool $include Whether to include the ability. Default true (after declarative filters pass).
536+
* @param WP_Ability $ability The ability instance being evaluated.
537+
* @param array $args The full $args array passed to wp_get_abilities().
538+
*/
539+
$include = (bool) apply_filters( 'wp_get_abilities_match', $include, $ability, $args );
540+
541+
if ( $include ) {
542+
$matched[] = $ability;
543+
}
544+
}
545+
546+
// Step 4: Caller-scoped result callback.
547+
if ( null !== $result_callback ) {
548+
$matched = (array) call_user_func( $result_callback, $matched );
549+
}
550+
551+
/**
552+
* Filters the full list of matched abilities after all per-item filtering is complete.
553+
*
554+
* Fires after the caller-scoped result_callback. Plugins can use this to sort,
555+
* paginate, or reshape the final result set universally.
556+
*
557+
* @since 7.1.0
558+
*
559+
* @param WP_Ability[] $matched The matched abilities after all filtering.
560+
* @param array $args The full $args array passed to wp_get_abilities().
561+
*/
562+
return (array) apply_filters( 'wp_get_abilities_result', $matched, $args );
563+
}
564+
565+
/**
566+
* Checks whether an ability's meta array matches a set of required key/value conditions.
567+
*
568+
* All conditions must match (AND logic). Supports nested arrays for structured meta,
569+
* e.g. `array( 'mcp' => array( 'public' => true ) )`.
570+
*
571+
* @since 7.1.0
572+
* @access private
573+
*
574+
* @param array $meta The ability's meta array.
575+
* @param array $conditions The required key/value conditions to match against.
576+
* @return bool True if all conditions match, false otherwise.
577+
*/
578+
function wp_get_abilities_match_meta( array $meta, array $conditions ): bool {
579+
foreach ( $conditions as $key => $value ) {
580+
if ( ! array_key_exists( $key, $meta ) ) {
581+
return false;
582+
}
583+
584+
if ( is_array( $value ) ) {
585+
if ( ! is_array( $meta[ $key ] ) || ! wp_get_abilities_match_meta( $meta[ $key ], $value ) ) {
586+
return false;
587+
}
588+
} elseif ( $meta[ $key ] !== $value ) {
589+
return false;
590+
}
591+
}
592+
593+
return true;
426594
}
427595

428596
/**

src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -86,26 +86,20 @@ public function register_routes(): void {
8686
* @return WP_REST_Response Response object on success.
8787
*/
8888
public function get_items( $request ) {
89-
$abilities = array_filter(
90-
wp_get_abilities(),
91-
static function ( $ability ) {
92-
return $ability->get_meta_item( 'show_in_rest' );
93-
}
89+
$query_args = array(
90+
'meta' => array( 'show_in_rest' => true ),
9491
);
9592

96-
// Filter by ability category if specified.
97-
$category = $request['category'];
98-
if ( ! empty( $category ) ) {
99-
$abilities = array_filter(
100-
$abilities,
101-
static function ( $ability ) use ( $category ) {
102-
return $ability->get_category() === $category;
103-
}
104-
);
105-
// Reset array keys after filtering.
106-
$abilities = array_values( $abilities );
93+
if ( ! empty( $request['category'] ) ) {
94+
$query_args['category'] = $request['category'];
95+
}
96+
97+
if ( ! empty( $request['namespace'] ) ) {
98+
$query_args['namespace'] = $request['namespace'];
10799
}
108100

101+
$abilities = wp_get_abilities( $query_args );
102+
109103
$page = $request['page'];
110104
$per_page = $request['per_page'];
111105
$offset = ( $page - 1 ) * $per_page;
@@ -432,12 +426,18 @@ public function get_collection_params(): array {
432426
'minimum' => 1,
433427
'maximum' => 100,
434428
),
435-
'category' => array(
429+
'category' => array(
436430
'description' => __( 'Limit results to abilities in specific ability category.' ),
437431
'type' => 'string',
438432
'sanitize_callback' => 'sanitize_key',
439433
'validate_callback' => 'rest_validate_request_arg',
440434
),
435+
'namespace' => array(
436+
'description' => __( 'Limit results to abilities in a specific namespace.' ),
437+
'type' => 'string',
438+
'sanitize_callback' => 'sanitize_key',
439+
'validate_callback' => 'rest_validate_request_arg',
440+
),
441441
);
442442
}
443443
}

0 commit comments

Comments
 (0)