Skip to content
This repository was archived by the owner on Feb 5, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 133 additions & 1 deletion includes/abilities-api/class-wp-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*
* @see WP_Abilities_Registry
*/
class WP_Ability {
class WP_Ability implements \JsonSerializable {

/**
* The default value for the `show_in_rest` meta.
Expand Down Expand Up @@ -340,6 +340,138 @@ public function get_meta_item( string $key, $default_value = null ) {
return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value;
}

/**
* Converts the ability to an array representation.
*
* Returns a complete array representation of the ability including name, label,
* description, schemas, and metadata. Callbacks are excluded as they are not serializable.
*
* @since n.e.x.t
*
* @return array<string,mixed> {
* The ability as an associative array.
*
* @type string $name The ability name with namespace.
* @type string $label The human-readable label.
* @type string $description The detailed description.
* @type array $input_schema The input validation schema.
* @type array $output_schema The output validation schema.
* @type array $meta {
* Metadata for the ability.
*
* @type array $annotations {
* Behavior annotations.
*
* @type string $instructions Usage instructions.
* @type bool $readonly Whether the ability is read-only.
* @type bool $destructive Whether the ability is destructive.
* @type bool $idempotent Whether the ability is idempotent.
* }
* @type bool $show_in_rest Whether the ability is exposed in REST API.
* }
* }
*/
public function to_array(): array {
Comment thread
bordoni marked this conversation as resolved.
$array = array(
'name' => $this->get_name(),
'label' => $this->get_label(),
'description' => $this->get_description(),
'input_schema' => $this->get_input_schema(),
'output_schema' => $this->get_output_schema(),
'meta' => $this->get_meta(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here, to make the PHPDoc correct, you would have to account for the callback in any custom meta provided or limit that object to annotations and show_in_rest.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point I will change the docblock, as I feel like the to_array() should allow other meta.

);

/**
* Filters the array representation of an ability.
*
* @since n.e.x.t
*
* @param array<string,mixed> $array The ability as an associative array.
* @param \WP_Ability $ability The ability instance.
*/
return apply_filters( "wp_ability_{$this->get_name()}_to_array", $array, $this );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some example from WP core:

I don't see filters used there. @justlevine shared some concerns regarding using filters on methods that are executed multiple times in #37 (comment). More broadly, I'm curious what scenarios this helper method would require these filters, taking into account that the consumer can still manipulate the result however they like.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Filtering here allows individual modification of the "context" for the AI models later on, which is not required for the other methods like this one

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you share an example of how this ::to_array() would be consumed to pass to the AI model? It isn't used this way in the codebase currently, so I might be missing some nuance here.

@justlevine justlevine Oct 13, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@gziolo thanks for tagging me 🙇

IMO semantically it would make sense that a filter on to_array() would trigger every time a WP_Ability is transformed, so I don't have that same concern here.

I also like @bordoni 's use case, since that sort of tailoring wouldn't really make sense extending the class to overload this function. (Though would like to know more as to what modifications you would want injected at that point in the lifecycle and not earlier).

That said, I do think it's perfectly fine for WordPress 6.9 to not allow devs to inject top-level properties via a hook. They can already filter args, and dump whatever they want in meta, so another filter feels like something we can do additively in 7.0 and later.

(Per previous decisions we can merge now and decide whether to strip it during beta).

@felixarntz felixarntz Oct 14, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When wanting to keep structures concrete, filters are not a great tool :)

It's a problem that's discussed over and over, every once in a while it comes up, but obviously, in a filter callback one can do anything one likes. I am not opposed to adding a filter here, but I am much more of a proponent of adding one based on concrete needs.

In WordPress Core, every API interface you add will have to be there more or less forever. You can't remove it. But you can always add something. So unless there's a critical need we know of ahead of time, I'd always vote for most minimal public API surfaces possible.

If we still want to proceed with adding a filter here right away, I'd strongly suggest to not have it run on the entire data. The use-case mentioned is to inject additional key-value pairs - that's totally reasonable and can be done by filtering an empty array, then merging it into the base result. This way, we allow adding stuff, but we don't allow messing up the base result, which is critical to ensure data integrity.

Alternatively, to achieve the same goal, create a copy of $array first, then filter the copy (this way the filter retains access to the whole associative array and can operate on it), and after the filter application merge the original array back into the copy (to ensure the original data is still the same). The filter docs would then need to mention that this can only be used to add additional keys, not to change existing keys.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there already an existing use case for the filters, or is this a case of "we expect people will want this"? It's not a popular opinion in the WP community, but I'm a big fan of not adding filters to early versions of things that don't have a lot of use yet. It's early days and we may need to change things we can't realize yet. In other words, I'm a fan of code that's easy to change, at this stage of development.

Like @felixarntz says above, every API interface you add basically has to remain forever, unless/until it's deprecated and replaced by something else. In this case, we have PHP classes, methods, and filters, all of which are public and a form of an API, and all of which are harder to change once they're in the wild.

If there are already solid needs for the filters, then the additive approach mentioned by Felix seems the better way to go.

}

/**
* Serializes the ability to a value that can be serialized natively by json_encode().
*
* Implements the JsonSerializable interface to allow the ability to be passed
* directly to json_encode() without manually calling to_array().
*
* @since n.e.x.t
*
* @return array<string,mixed> The ability as an associative array.
*/
public function jsonSerialize(): array {
return $this->to_array();
}

/**
* Converts the ability to a JSON Schema representation.
*
* Generates a JSON Schema Draft 7 compliant schema describing the ability's
* structure, including input/output schemas and metadata.
*
* @since n.e.x.t
*
* @return array<string,mixed> A JSON Schema representation of the ability.
*/
public function to_json_schema(): array {
$input_schema = $this->get_input_schema();
$output_schema = $this->get_output_schema();

$schema = array(
'$schema' => 'http://json-schema.org/draft-07/schema#',
'type' => 'object',
'title' => $this->get_label(),
'description' => $this->get_description(),
'properties' => array(
'name' => array(
'type' => 'string',
'const' => $this->get_name(),
),
'meta' => array(
'type' => 'object',
'properties' => array(
'annotations' => array(
'type' => 'object',
'properties' => array(
'instructions' => array( 'type' => 'string' ),
'readonly' => array( 'type' => 'boolean' ),
'destructive' => array( 'type' => 'boolean' ),
'idempotent' => array( 'type' => 'boolean' ),
),
),
'show_in_rest' => array(
'type' => 'boolean',
),
),
),
),
'required' => array( 'name', 'meta' ),
);

if ( ! empty( $input_schema ) ) {
$schema['properties']['input_schema'] = $input_schema;
$schema['required'][] = 'input_schema';
}

if ( ! empty( $output_schema ) ) {
$schema['properties']['output_schema'] = $output_schema;
$schema['required'][] = 'output_schema';
}

/**
* Filters the JSON Schema representation of an ability.
*
* @since n.e.x.t
*
* @param array<string,mixed> $schema The JSON Schema representation.
* @param \WP_Ability $ability The ability instance.
*/
return apply_filters( "wp_ability_{$this->get_name()}_to_json_schema", $schema, $this );
}

/**
* Validates input data against the input schema.
*
Expand Down
Loading