[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