@@ -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/**
0 commit comments