[wp-trac] [WordPress Trac] #64990: Abilities API: Add filtering support to `wp_get_abilities()`

WordPress Trac noreply at wordpress.org
Mon Mar 30 09:35:03 UTC 2026


#64990: Abilities API: Add filtering support to `wp_get_abilities()`
-------------------------+-----------------------------
 Reporter:  gziolo       |       Owner:  (none)
     Type:  enhancement  |      Status:  new
 Priority:  normal       |   Milestone:  Future Release
Component:  AI           |     Version:  6.9
 Severity:  normal       |  Resolution:
 Keywords:  abilities    |     Focuses:
-------------------------+-----------------------------

Comment (by gziolo):

 === Proposed solution

 Extend {{{wp_get_abilities()}}} to accept an optional {{{$args}}} array
 parameter. When {{{$args}}} is provided, the function filters the registry
 and returns the matching subset. When called without arguments, behavior
 is unchanged and returns all abilities as {{{WP_Ability[]}}}.

 No new classes are introduced for this path. Filtering logic lives inside
 {{{wp_get_abilities()}}} (or a private helper it delegates to), following
 the established WordPress convention of array-based {{{$args}}}
 ({{{get_posts}}}, {{{get_terms}}}). Since {{{wp_get_abilities()}}} already
 returns {{{WP_Ability[]}}}, changing its return type to a Collection
 object would break existing call sites, so the {{{$args}}} approach is the
 natural fit for the existing function.

 That said, the Collection approach explored in
 [https://github.com/WordPress/abilities-api/pull/119 PR #119] has real
 developer experience appeal, and I'd be open to it as a '''parallel entry
 point''': e.g., a separate {{{wp_get_abilities_collection()}}} function
 that wraps the same registry. This would let developers who prefer the
 fluent style opt into it without affecting backward compatibility.
 However, establishing the foundational {{{$args}}}-based filtering first
 gives both paths a shared filtering primitive to build on, so I'd suggest
 starting here.

 ==== Phase 1: Category and namespace filtering

 Aligns the PHP API with what the REST API already supports for categories,
 and adds the namespace filtering that WooCommerce and other consumers have
 been building ad-hoc.

 {{{#!php
 // Filter by category (single or array, OR logic within).
 $content_abilities = wp_get_abilities( array( 'category' => 'content' ) );

 // Filter by namespace.
 $woo_abilities = wp_get_abilities( array( 'namespace' => 'woocommerce' )
 );

 // Combine (AND logic between different arg types).
 $woo_content = wp_get_abilities( array(
     'category'  => 'content',
     'namespace' => 'woocommerce',
 ) );

 // Multiple values use OR logic within the same arg type.
 $abilities = wp_get_abilities( array(
     'category' => array( 'content', 'settings' ),
 ) );
 }}}

 The REST API controller should be refactored to delegate to
 {{{wp_get_abilities( $args )}}} internally, eliminating its own post-
 retrieval filtering. A {{{namespace}}} query parameter should be added to
 the REST endpoint to match.

 ==== Phase 2: Meta filtering

 Enables internal refactoring of the MCP adapter, WebMCP adapter, and REST
 controller visibility checks — all of which currently do their own
 {{{array_filter}}} pass over meta properties.

 {{{#!php
 // Abilities exposed over REST.
 $rest_abilities = wp_get_abilities( array(
     'meta' => array( 'show_in_rest' => true ),
 ) );

 // Abilities exposed over MCP.
 $mcp_abilities = wp_get_abilities( array(
     'meta' => array(
         'show_in_rest' => true,
         'mcp'          => array( 'public' => true ),
     ),
 ) );
 }}}

 Meta filters use AND logic, so all specified conditions must match. Nested
 keys are supported for structured metadata like {{{mcp.public}}}. It
 should be added to the REST endpoint as well.

 ==== Phase 3: Caller-scoped callbacks

 Two {{{$args}}} keys that give the caller control over per-item inclusion
 and final result shaping, without touching global state.

 '''{{{match_callback}}}''' — receives each ability that survived the
 declarative filters. Returns {{{true}}} to include, {{{false}}} to
 exclude. Covers cases that {{{category}}}, {{{namespace}}}, and {{{meta}}}
 cannot express: OR conditions across meta fields, role-based visibility,
 protocol-specific gates. This was flagged as a need during review of the
 prior proposals.

 {{{#!php
 // Per-item: only abilities the current user can execute.
 $abilities = wp_get_abilities( array(
     'category'       => 'content',
     'match_callback' => function ( WP_Ability $ability ) {
         return current_user_can( $ability->get_meta()['capability'] ??
 'manage_options' );
     },
 ) );
 }}}

 '''{{{result_callback}}}''' — receives the full matched array after all
 per-item filtering is done. Returns the transformed array. Lets the caller
 sort, slice, or reshape the result in a single self-contained call.

 {{{#!php
 // Result-level: sort and paginate without global filters.
 $abilities = wp_get_abilities( array(
     'category'        => 'content',
     'result_callback' => function ( array $abilities ) {
         usort( $abilities, function ( WP_Ability $a, WP_Ability $b ) {
             return strcasecmp( $a->get_label(), $b->get_label() );
         } );
         return array_slice( $abilities, 0, 10 );
     },
 ) );
 }}}

 ==== Phase 4: Ecosystem-scoped hooks

 Today, plugin authors who need filtered abilities call
 {{{wp_get_abilities()}}} and apply their own logic after the fact. This
 works for the individual caller, but it means no other plugin can
 influence that filtering — there is no hook point between retrieval and
 consumption. A security plugin cannot enforce capability checks, the MCP
 adapter cannot gate visibility, and core cannot apply default scoping.
 Each consumer is an island.

 By moving filtering //inside// {{{wp_get_abilities()}}}, the pipeline
 ensures that ecosystem hooks fire in a defined order, giving plugins a
 reliable place to participate. Each callback from Phase 3 has a
 corresponding filter that lets the ecosystem inject logic universally,
 regardless of what the caller passed.

 '''{{{wp_get_abilities_match}}}''' — fires per-item, after
 {{{match_callback}}}. Any plugin can hook this to enforce inclusion rules
 globally. For example, the MCP adapter could enforce {{{mcp.public}}}
 visibility, or a security plugin could restrict abilities by role. The
 filter receives whether the ability matched so far ({{{$match}}}), the
 {{{WP_Ability}}} instance, and the full {{{$args}}}.

 '''{{{wp_get_abilities_result}}}''' — fires once on the full array, after
 {{{result_callback}}}. Lets plugins shape the final output — sorting,
 pagination, reordering by priority. The filter receives the
 {{{WP_Ability[]}}} array and the full {{{$args}}}. Rather than baking
 {{{orderby}}}, {{{order}}}, {{{limit}}}, and {{{offset}}} into the
 {{{$args}}} signature, this hook lets those concerns be handled by plugins
 (e.g., the REST API controller applying its own pagination) without
 growing the core API surface.

 Ecosystem hooks fire last at each level, so plugins always get the final
 say.

 ==== Pipeline summary

  1. Declarative filters ({{{category}}}, {{{namespace}}}, {{{meta}}}) —
 per-item
  2. {{{match_callback}}} — per-item, caller-scoped
  3. {{{wp_get_abilities_match}}} filter — per-item, ecosystem-scoped
  4. {{{result_callback}}} — on the full array, caller-scoped
  5. {{{wp_get_abilities_result}}} filter — on the full array, ecosystem-
 scoped

 Steps 1–3 run inside a single loop — no extra iteration.

 ==== Design notes

  * '''AND between arg types, OR within multi-value args''' — matches
 WordPress convention.
  * '''Single pass''' — all conditions (category, namespace, meta,
 callback) are evaluated per-ability in one loop, avoiding multiple
 iterations over the registry.
  * '''REST parity''' — every {{{$args}}} key should correspond to a
 supported REST API query parameter where it makes sense.
  * '''Separation of concerns''' — the function handles //selection//
 (declarative args); callbacks handle //caller-scoped// logic
 ({{{match_callback}}}, {{{result_callback}}}), while filters handle
 //ecosystem-scoped// logic ({{{wp_get_abilities_match}}},
 {{{wp_get_abilities_result}}}). Each layer has a clear owner without
 overlap.

 ==== Out of scope

  * Fluent/collection-style API — worth exploring as a parallel entry point
 (e.g., {{{wp_get_abilities_collection()}}}) once the foundational
 {{{$args}}} filtering is in place. The developer experience benefits are
 clear. The main reason not to start there is backward compatibility with
 the existing return type.
  * A universal {{{public}}} meta flag cascading into protocol-specific
 visibility (tracked separately as part of the WebMCP / MCP adapter
 discussions).

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/64990#comment:1>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list