Skip to content

Conversation

@galatanovidiu
Copy link
Contributor

@galatanovidiu galatanovidiu commented Oct 14, 2025

Summary

Closes #38.

Adds querying and filtering capabilities to the Abilities API, allowing developers to efficiently find and retrieve specific abilities from potentially thousands of registered abilities.

This PR introduces the WP_Abilities_Query class and extends wp_get_abilities() to accept filtering arguments, enabling server-side filtering by category, namespace, meta properties, search terms, and more.

Changes

New Features

  • New WP_Abilities_Query class - WordPress-standard query interface for filtering abilities
  • Enhanced wp_get_abilities() function - Now accepts optional $args parameter for filtering
  • Multiple filter types:
    • category - Filter by category slug (single or array)
    • namespace - Filter by namespace (single or array)
    • search - Search in name, label, and description
    • meta - Filter by meta properties (show_in_rest, annotations, custom meta)
    • orderby - Sort by name, label, or category
    • order - Sort direction (ASC/DESC)
    • limit/offset - Pagination support

Implementation Details

  • All filters use AND logic between different filter types
  • Multiple values in category or namespace use OR logic
  • Meta filters support nested arrays with AND logic
  • Single-pass filtering for optimal performance

Documentation

  • New comprehensive documentation in docs/8.querying-filtering-abilities.md
  • Updated docs/4.using-abilities.md with filtering examples
  • Includes a quick reference table and detailed examples

Backward Compatibility

Fully backward compatible - existing code calling wp_get_abilities() without arguments continues to work unchanged.

@github-actions
Copy link

github-actions bot commented Oct 14, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: galatanovidiu <[email protected]>
Co-authored-by: JasonTheAdams <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: justlevine <[email protected]>
Co-authored-by: jonathanbossenger <[email protected]>
Co-authored-by: emdashcodes <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@codecov
Copy link

codecov bot commented Oct 14, 2025

Codecov Report

❌ Patch coverage is 93.82716% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.66%. Comparing base (c1b24b8) to head (8a5c01f).
⚠️ Report is 1 commits behind head on trunk.

Files with missing lines Patch % Lines
...ncludes/abilities-api/class-wp-abilities-query.php 94.93% 8 Missing ⚠️
includes/bootstrap.php 0.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##              trunk     #115      +/-   ##
============================================
+ Coverage     86.65%   87.66%   +1.00%     
- Complexity      148      211      +63     
============================================
  Files            18       19       +1     
  Lines           982     1143     +161     
  Branches         92       92              
============================================
+ Hits            851     1002     +151     
- Misses          131      141      +10     
Flag Coverage Δ
javascript 93.04% <ø> (ø)
unit 86.30% <93.82%> (+1.60%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Public constants are converted to private static properties

Additionally, validation methods are updated to use guard clauses, and some boolean logic is simplified.
Copy link
Member

@JasonTheAdams JasonTheAdams left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, @galatanovidiu! Left a few suggestions!

* @since n.e.x.t
* @var int
*/
private static $no_limit = -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a static property instead of a constant? Same for the static properties below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here should be fine to use const like here:

protected const DEFAULT_SHOW_IN_REST = false;

For arrays, PHP 7.2 is unhappy, so here:

protected static $default_annotations = array(

It's a complex env to support WP core 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean. Why would PHP 7.2 be unhappy for arrays as constant values? I believe arrays as scalar values was introduced in PHP 5.6.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, it’s linter that is very strict and prohibits usage. That would be similar case to array short syntax.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update the linter? const FOO = array( 'bar' => 123 ); is supported, and reducing our architecture quality to appease linting feels lame. Hahah!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I’m aware that was discussed at WC US, and someone has to champion the proposal and adjustments to the codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I initially implemented these as constants, but our PHP linter was flagging them with errors. To maintain compatibility with our linting rules (which appear to be stricter than PHP 7.2's actual capabilities), I converted them to private static properties.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's unfortunate. Is this the right place to propose the change? https://github.com/WordPress/WordPress-Coding-Standards?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the Core Committer meeting notes, which include an entire section on the need for changes in coding standards. You can try in that GitHub repository, or raise it as a topic for the weekly dev chat (next one today).

Comment on lines 71 to 81
* @phpstan-param array{
* category?: string|array<string>,
* namespace?: string|array<string>,
* search?: string,
* meta?: array<string,mixed>,
* orderby?: string,
* order?: string,
* limit?: int,
* offset?: int,
* ...<string, mixed>
* } $args
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's define this as a type in the class docblock, and then use @phpstan-import-type in the wp_get_abilities function. We can then set that as the @param type in this method, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried this, but it looks like @phpstan-import-type doesn't work in function docblocks (only in class docblocks)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct! It would go like this:

  1. Add an AbilityQueryArgs type in the class docblock
  2. Use it as the @param type for this method
  3. Import and use it in the function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly what I tried and is not working.

@phpstan-import-type only works in class docblocks, not in function docblocks. PHPStan throws an error: "Parameter $args has invalid type AbilityQueryArgs".

The solution is to keep the full @phpstan-param array shape in both places:

  1. Define @phpstan-type AbilityQueryArgs in the class docblock as the source of truth
  2. Duplicate the full array shape using @phpstan-param in the function docblock

This reduces duplication within the class but we still need to duplicate it for standalone functions since @phpstan-import-type doesn't work outside of class contexts.

Please check d3275fd

Comment on lines 109 to 112
$this->validate_meta_arg();
$this->validate_orderby();
$this->validate_order();
$this->validate_pagination_args();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, these validation methods are interesting. They "validate" in the sense that they check if the provided value is valid, but then fallback on a default instead of notifying of the issue in any way. Is this how the WP_Query class works, so we're doing this for consistency? I feel like we should at least be calling doing_it_wrong so people know why there's unexpected behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have used sanitize instead of validate
WP_Query also only sanitizes the args.
I can implement validation if you want, but I'd prefer to keep it simple.

Please check 68fea1b

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, gotcha. I'm inclined to let people know when they're straight up doing things wrong (I've personally felt the consternation of figuring out that I have a typo in a query), but won't push it if it's not recommended otherwise.

usort(
$abilities,
static function ( $a, $b ) use ( $getter_method, $order_multiplier ) {
return strcasecmp( $a->$getter_method(), $b->$getter_method() ) * $order_multiplier;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, I like the use of $order_multiplier here! Clever! 😃

WP_Abilities_Query already returns all abilities when args is empty, making the backward compatibility check unnecessary.
Updates type hints to specify arrays of strings for 'category' and 'namespace' parameters.
Updates method names and related docblocks to clarify that query argument processing focuses on sanitization rather than validation.
Improves documentation to specify use of the query class for retrieving and filtering abilities.
*
* @since n.e.x.t
*/
class WP_Abilities_Query {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the docs it seems this class isn't meant to be consumed/extended directly (which IMO makes sense from a 6.9 iterative POV).

If so, can we mark the class final and add an @internal + a note pointing to wp_get_abilities( $args ) in the doc-block? Would give us more freedom to iterate in future versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

@justlevine justlevine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned about using the WP_*_Query when that pattern is

  1. used by core for DB lookups
  2. often encouraged to be used directly versus getters (e.g. new WP_Term_Query() versus get_terms(). )

Neither afaik are true here and docs can only help so much.

From a code POV, we could also simplify implementation significantly if we weren't trying to mimic WP_Query. E.g. we don't need 011y, query getters, or pagination when we're just sorting/filtering an array in PHP.

* @since n.e.x.t
*
* @param array<string,mixed> $args Query arguments.
*
Copy link
Contributor

@justlevine justlevine Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'm assuming the blank end-lines in the comments throughout this file are unintentional

Suggested change
*

Copy link
Member

@JasonTheAdams JasonTheAdams Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really fair point, @justlevine. Thinking on this, I wonder if this would be a good use for introducing an Abilities_Collection class, instead of a querying system like this. It would implement IteratorAggregate, ArrayAccess, and such, and be what the registry stores all the abilities into.

From there, it could have methods such as Abilities_Collection::with_category(...$categories) which returns an array (or another Abilities_Collection subset). If it's the latter, then folks can chain:

$abilities = wp_get_abilities()
  ->with_namespace('woo')
  ->with_category('orders');

This way, to your point, we're treating it more like an in-memory array rather than querying against an external system. Constructing a query is most important for efficiently retrieving the data in one go. But for something like this it's not really necessary.

What do you think, including @galatanovidiu?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you feel this needs more time to define the API’s shape, we will have to postpone it to WordPress 7.0. This is a rather substantial change that needs some longer conversation, so contributors can have time to chime in and share their perspective on how the codebase evolves, so they can later replicate better patterns.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with all of you.
I know we are in a hurry to have this functionality out and ready for 6.9, but I also like the idea of the Abilities Collection a lot. I've used Laravel collections a lot, and I know how natural and DX-friendly that is.

Because of that, I've prepared a POC here: #119

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also important to consider performance. The proposal for collections will loop through once for the current set of abilities per filter. In contrast, in this PR, there is a single pass over the entire set, and all filters are applied to a single item at once. There is also an extensibility story that needs to be taken into account. WP_Query is at extreme with 44 occurrences of apply_filters() and apply_filters_ref_array(), but some sort of filtering will eventually need to be added, so custom filters can be executed, for example, when fetching abilities through the REST API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a rather substantial change that needs some longer conversation,

I second @gziolo, but probably take it further than he.

While I'm a big fan of fluent APIs in general (and love how much simpler #119 reads), I don't think that us introducing the concept of a *_Collection into WordPress on the cusp of 6.9 is a great idea. It's also possible that the WP_Query parrten will turn out to be ideal in 7.x after we find a reason to have abilities be backed by something in the DB (a la plugins/themes/fonts), in which case preemptively going with a Collection pattern would be hard to untangle.

I'm hoping we can take the simplified methods from @galatanovidiu 's #119 but move them to private functions inside WP_Abilities_Registry. That would give us all the surface area we need for MVP filtering support in 6.9 (albeit via passed $args), while still keeping our hands free for whatever pattern we ultimately decide is best (when we have some unpressured time to reach the decision holistically).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for weighing in, folks! And for throwing together the draft so quickly, @galatanovidiu! 😄

Overall, I agree with the desire to not rush this. If we start with simple arrays in 6.9 with no built-in filtering (folks can always get the abilities and filter them themselves), I'm fine with that.

Regarding the introduction of the *_Collection concept, in my opinion, WordPress errs too much on the side of using "we haven't done that before" argument as a reason not to do things. Concepts like Collections are tried and true patterns across multiple systems. We're not creating a new concept entirely, we're porting one over. I do appreciate you raising the point, @justlevine, as it is a good one to be mindful of. I would love to see more tried-and-true practices and patterns brought tastefully into WordPress over time.

With regards to performance, @galatanovidiu, in this case I think it's negligible — absolutely nothing like performance from a database perspective. And even chaining things like with_x is working from a smaller. subset each time, so the iteration performance jumps each time. Frankly, this isn't too unlike how databases often filter things down.

In short, I'm in favor of closing this PR to explore a better API. I really like the draft (which I need to dig into more), and would like to explore that sort of direction further. 😄

Comment on lines +201 to +206
// Paginate results
$abilities = wp_get_abilities( array(
'limit' => 10,
'offset' => 0,
) );
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific need for pagination?

I see that you're trying to mirror WP_Query et al (versus adding sorting/filtering as registry methods) but abilities are in a PHP array, not a database. Do we really need a shortcut for what's essentially an array_slice()?

Copy link
Member

@gziolo gziolo Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering abilities are the most crucial aspect from my perspective. Ordering and pagination are nice-to-haves, but I want to acknowledge that they fit well together in the proposed implementation, as they allow easy selection of a precise number of abilities that meet specific criteria, which might be useful when passing data to an LLM with limited context. @galatanovidiu might have a better overview of the real needs based on the work he has done for the MCP Adapter.

@gziolo
Copy link
Member

gziolo commented Oct 15, 2025

One thing I want to note is that some filters are hard to express in this array notation. For example, a category or namespace let you provide an array of options where the existence of one of them means a match (OR strategy). In the case of meta where the structure is more complex, the approach used (AND strategy) makes more sense, but this comes at the cost of the developer having to be very careful when studying the docs to ensure they express their intent appropriately. However, for meta it isn't currently possible to request an OR condition in case at least one of several meta conditions is met.

galatanovidiu and others added 3 commits October 15, 2025 18:14
Defines a new `@phpstan-type` called `AbilityQueryArgs` to represent the arguments for an abilities query. This centralizes the complex array shape definition, which was previously defined inline in the constructor's docblock.

Using this shared type improves code readability and maintainability. The `wp_get_abilities` function documentation is also updated to reference this new type for better consistency.
Marks the `WP_Abilities_Query` class as `final` and `@internal` to clarify that it is not part of the public API.

This change prevents the class from being extended or consumed directly, encouraging developers to use the stable `wp_get_abilities()` function instead. This helps ensure forward-compatibility and avoids potential breakage from future implementation changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement New feature or request

Projects

Status: Needs review

Development

Successfully merging this pull request may close these issues.

Proposal: Add a convienient way to filter the list of all registered abilities

4 participants