Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
55d75fc
Implement ability categories in the Abilities API
galatanovidiu Oct 6, 2025
df4af2c
Refactor ability category registration and retrieval logic
galatanovidiu Oct 6, 2025
b97a258
Refactor(Abilities): Change ability category from an array to a singl…
galatanovidiu Oct 6, 2025
579333e
Refactor(Abilities): Update registry for single category model
galatanovidiu Oct 6, 2025
8a7e62e
Refactor(Abilities): Update REST API for single category model
galatanovidiu Oct 6, 2025
38a46aa
Feat(Abilities): Validate that an ability's category is registered
galatanovidiu Oct 6, 2025
d2ac5cc
Fix: Correct PHPDoc type hints in WP_Abilities_Category_Registry
galatanovidiu Oct 6, 2025
8490c39
Refactor: Rename abilities_category_registry_init hook for consistency
galatanovidiu Oct 6, 2025
59f6634
Refactor: Improve ability filtering logic
galatanovidiu Oct 6, 2025
6a6b381
Chore: Add missing newlines at end of files
galatanovidiu Oct 6, 2025
42c7ace
test(abilities-api): Add tests for ability categories
galatanovidiu Oct 6, 2025
92a166e
test(abilities-api): Update unit tests to use ability categories
galatanovidiu Oct 6, 2025
3098037
test(rest-api): Update tests to use ability categories
galatanovidiu Oct 6, 2025
1bac363
test(rest-api): Add tests for category filtering and schema
galatanovidiu Oct 6, 2025
cd406da
docs: Document Ability Categories feature
galatanovidiu Oct 6, 2025
ff86ae2
docs: Update examples to use Ability Categories
galatanovidiu Oct 6, 2025
2a3bf05
docs: Document Category support in REST API
galatanovidiu Oct 6, 2025
9b17174
Refactor ability category registration validation
galatanovidiu Oct 7, 2025
6e42f67
Refactor tests to register ability categories on the init hook
galatanovidiu Oct 7, 2025
1f9e75c
Rename category registration hook
galatanovidiu Oct 7, 2025
b82607d
Remove ability filtering by category
galatanovidiu Oct 7, 2025
110d403
Improve tests for ability category validation
galatanovidiu Oct 8, 2025
f8bcd0b
Merge branch 'trunk' into feature/add-categories-system
galatanovidiu Oct 8, 2025
7e4e195
Fix: Correct formatting and assertions in unit tests
galatanovidiu Oct 8, 2025
9de15dd
Changes the version annotations from `0.3.0` to `n.e.x.t`
galatanovidiu Oct 9, 2025
7a5047b
Update version annotations from `0.3.0` to `n.e.x.t` in abilities cat…
galatanovidiu Oct 9, 2025
c093d32
Revert version to 0.2.0
galatanovidiu Oct 9, 2025
308e62c
Removes slug sanitization for ability categories
galatanovidiu Oct 9, 2025
6bde79a
test: Replace conditional checks with assertions for non-existent cat…
galatanovidiu Oct 9, 2025
474dcc9
Merge branch 'trunk' into feature/add-categories-system
galatanovidiu Oct 9, 2025
99a6eb8
Fix: Add missing category to show_in_rest test abilities
galatanovidiu Oct 9, 2025
5b8adbb
Added a comment to the __wakeup method to clarify that unserializatio…
galatanovidiu Oct 9, 2025
f19f0cb
Add meta support to ability categories
galatanovidiu Oct 9, 2025
2bb837d
Remove validation for ability category slug format
galatanovidiu Oct 10, 2025
cfe3c24
Change WP_Ability_Category class to final
galatanovidiu Oct 10, 2025
f3ae972
Merge branch 'trunk' into feature/add-categories-system
galatanovidiu Oct 10, 2025
150461e
Fix: Correct assertion in REST API ability get_item test
galatanovidiu Oct 10, 2025
ebdcfb1
Refactor: Improve ability category cleanup in tests
galatanovidiu Oct 10, 2025
a573fd7
Chore: Fix indentation in WP_Ability class
galatanovidiu Oct 10, 2025
ab23e20
Docs: Move category registration to a dedicated file
galatanovidiu Oct 10, 2025
56ccd79
Harden: Prevent serialization of ability and registry classes
galatanovidiu Oct 13, 2025
7a66dcf
docs: clarify category registration step
galatanovidiu Oct 13, 2025
88fe7a6
Update translator comment and string to specify “Ability category” fo…
galatanovidiu Oct 13, 2025
b96c89d
Update translator comment and string to specify “Ability category” fo…
galatanovidiu Oct 13, 2025
84c77e4
Update translator comment and string to specify “Ability category” fo…
galatanovidiu Oct 13, 2025
aee17c8
Docs: Remove documentation for category registration filter
galatanovidiu Oct 13, 2025
2e2992c
Fix PHPStan annotations for category registration
galatanovidiu Oct 13, 2025
be1b20e
Update tests/unit/abilities-api/wpAbilityCategory.php
galatanovidiu Oct 13, 2025
990e77a
Refactor category slug tests to use @dataProvider
galatanovidiu Oct 13, 2025
2414c2f
Refactor test_get_all_categories to use register_category_during_hook
galatanovidiu Oct 13, 2025
b724ae7
Remove 'annotations' field from abilities schema and update required …
galatanovidiu Oct 13, 2025
48c3ba1
Update required fields in abilities schema to include 'input_schema' …
galatanovidiu Oct 13, 2025
e69d69b
Add category property tests to abilities schema validation
galatanovidiu Oct 13, 2025
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
16 changes: 14 additions & 2 deletions docs/1.intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ It acts as a central registry, making it easier for different parts of WordPress

## Core Concepts

- **Ability:** A distinct piece of functionality with a unique name following the `namespace/ability-name` pattern. Each ability has a human-readable name and description, input/output definitions (using JSON Schema), optional permissions, and an associated callback function for execution. Each registered Ability is an instance of the `WP_Ability` class.
- **Registry:** A central, singleton object (`WP_Abilities_Registry`) that holds all registered abilities. It provides methods for registering, unregistering, finding, and querying abilities.
- **Ability:** A distinct piece of functionality with a unique name following the `namespace/ability-name` pattern. Each ability has a human-readable name and description, input/output definitions (using JSON Schema), a category assignment, optional permissions, and an associated callback function for execution. Each registered Ability is an instance of the `WP_Ability` class.
- **Category:** A way to organize related abilities. Each ability must belong to exactly one category. Categories have a slug, label, and description. Each registered category is an instance of the `WP_Ability_Category` class.
- **Registry:** A central, singleton object (`WP_Abilities_Registry`) that holds all registered abilities. It provides methods for registering, unregistering, finding, and querying abilities. Similarly, `WP_Abilities_Category_Registry` manages all registered categories.
- **Callback:** The PHP function or method executed when an ability is called via `WP_Ability::execute()`.
- **Schema:** JSON Schema definitions for an ability's expected input (`input_schema`) and its returned output (`output_schema`). This allows for validation and helps agents understand how to use the ability.
- **Permission Callback:** An optional function that determines if the current user can execute a specific ability.
Expand All @@ -35,11 +36,22 @@ It acts as a central registry, making it easier for different parts of WordPress
## Registration Example

```php
// First, register a category, or use one of the existing categories.
add_action( 'abilities_api_categories_init', 'my_plugin_register_category');
function my_plugin_register_category(){
wp_register_ability_category( 'site-information', array(
'label' => __( 'Site Information', 'my-plugin' ),
'description' => __( 'Abilities that provide information about the WordPress site.', 'my-plugin' ),
));
}

// Then, register an ability in that category
add_action( 'abilities_api_init', 'my_plugin_register_ability');
function my_plugin_register_ability(){
wp_register_ability( 'my-plugin/site-info', array(
'label' => __( 'Site Info', 'my-plugin' ),
'description' => __( 'Returns information about this WordPress site', 'my-plugin' ),
'category' => 'site-information',
'input_schema' => array(),
'output_schema' => array(
'type' => 'object',
Expand Down
5 changes: 5 additions & 0 deletions docs/3.registering-abilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The `$args` array accepts the following keys:

- `label` (`string`, **Required**): A human-readable name for the ability. Used for display purposes. Should be translatable.
- `description` (`string`, **Required**): A detailed description of what the ability does, its purpose, and its parameters or return values. This is crucial for AI agents to understand how and when to use the ability. Should be translatable.
- `category` (`string`, **Required**): The slug of the category this ability belongs to. The category must be registered before registering the ability using `wp_register_ability_category()`. Categories help organize and filter abilities by their purpose. See [Registering Categories](7.registering-categories.md) for details.
- `input_schema` (`array`, **Required**): A JSON Schema definition describing the expected input parameters for the ability's execute callback. Used for validation and documentation.
- `output_schema` (`array`, **Required**): A JSON Schema definition describing the expected format of the data returned by the ability. Used for validation and documentation.
- `execute_callback` (`callable`, **Required**): The PHP function or method to execute when this ability is called.
Expand Down Expand Up @@ -55,6 +56,7 @@ function my_plugin_register_site_info_ability() {
wp_register_ability( 'my-plugin/get-site-info', array(
'label' => __( 'Get Site Information', 'my-plugin' ),
'description' => __( 'Retrieves basic information about the WordPress site including name, description, and URL.', 'my-plugin' ),
'category' => 'data-retrieval',
'input_schema' => array(
'type' => 'object',
'properties' => array(),
Expand Down Expand Up @@ -104,6 +106,7 @@ function my_plugin_register_update_option_ability() {
wp_register_ability( 'my-plugin/update-option', array(
'label' => __( 'Update WordPress Option', 'my-plugin' ),
'description' => __( 'Updates the value of a WordPress option in the database. Requires manage_options capability.', 'my-plugin' ),
'category' => 'data-modification',
'input_schema' => array(
'type' => 'object',
'properties' => array(
Expand Down Expand Up @@ -169,6 +172,7 @@ function my_plugin_register_woo_stats_ability() {
wp_register_ability( 'my-plugin/get-woo-stats', array(
'label' => __( 'Get WooCommerce Statistics', 'my-plugin' ),
'description' => __( 'Retrieves basic WooCommerce store statistics including total orders and revenue.', 'my-plugin' ),
'category' => 'ecommerce',
'input_schema' => array(
'type' => 'object',
'properties' => array(
Expand Down Expand Up @@ -222,6 +226,7 @@ function my_plugin_register_send_email_ability() {
wp_register_ability( 'my-plugin/send-email', array(
'label' => __( 'Send Email', 'my-plugin' ),
'description' => __( 'Sends an email to the specified recipient using WordPress mail functions.', 'my-plugin' ),
'category' => 'communication',
'input_schema' => array(
'type' => 'object',
'properties' => array(
Expand Down
4 changes: 4 additions & 0 deletions docs/5.rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Abilities are represented in JSON with the following structure:
"name": "my-plugin/get-site-info",
"label": "Get Site Information",
"description": "Retrieves basic information about the WordPress site.",
"category": "site-information",
"output_schema": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -63,6 +64,7 @@ Abilities are represented in JSON with the following structure:

- `page` _(integer)_: Current page of the collection. Default: `1`.
- `per_page` _(integer)_: Maximum number of items to return per page. Default: `50`, Maximum: `100`.
- `category` _(string)_: Filter abilities by category slug.

### Example Request

Expand All @@ -78,6 +80,7 @@ curl https://example.com/wp-json/wp/v2/abilities
"name": "my-plugin/get-site-info",
"label": "Get Site Information",
"description": "Retrieves basic information about the WordPress site.",
"category": "site-information",
"output_schema": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -128,6 +131,7 @@ curl https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info
"name": "my-plugin/get-site-info",
"label": "Get Site Information",
"description": "Retrieves basic information about the WordPress site.",
"category": "site-information",
"output_schema": {
"type": "object",
"properties": {
Expand Down
70 changes: 70 additions & 0 deletions docs/6.hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,49 @@ The Abilities API provides [WordPress Action and Filter Hooks](https://developer
## Quick Links

- [Actions](#actions)
- [`abilities_api_categories_init`](#abilities_api_categories_init)
Copy link
Member

@gziolo gziolo Oct 9, 2025

Choose a reason for hiding this comment

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

Outside of this PR, mostly note to myself and @jonathanbossenger, we are missing abilities_api_init covered here. That one is fundamental 😄

- [`before_execute_ability`](#before_execute_ability)
- [`after_execute_ability`](#after_execute_ability)
- [Filters](#filters)
- [`register_ability_args`](#register_ability_args)
- [`register_ability_category_args`](#register_ability_category_args)

## Actions

### `abilities_api_categories_init`

Fires when the category registry is first initialized. This is the proper hook to use when registering categories.

```php
do_action( 'abilities_api_categories_init', $registry );
```

#### Parameters

- `$registry` (`\WP_Abilities_Category_Registry`): The category registry instance.

#### Usage Example

```php
/**
* Register custom ability categories.
*
* @param \WP_Abilities_Category_Registry $registry The category registry instance.
*/
function my_plugin_register_categories( $registry ) {
wp_register_ability_category( 'ecommerce', array(
'label' => __( 'E-commerce', 'my-plugin' ),
'description' => __( 'Abilities related to e-commerce functionality.', 'my-plugin' ),
));

wp_register_ability_category( 'analytics', array(
'label' => __( 'Analytics', 'my-plugin' ),
'description' => __( 'Abilities that provide analytical data and insights.', 'my-plugin' ),
));
}
add_action( 'abilities_api_categories_init', 'my_plugin_register_categories' );
```

### `before_execute_ability`

Fires immediately before an ability gets executed, after permission checks have passed but before the execution callback is called.
Expand Down Expand Up @@ -132,3 +168,37 @@ function my_modify_ability_args( array $args, string $ability_name ): array {
}
add_filter( 'register_ability_args', 'my_modify_ability_args', 10, 2 );
```

### `register_ability_category_args`

Allows modification of a category's arguments before validation.

```php
$args = apply_filters( 'register_ability_category_args', array $args, string $slug );
```

#### Parameters

- `$args` (`array<string,mixed>`): The arguments used to instantiate the category (label, description).
- `$slug` (`string`): The slug of the category being registered.

#### Usage Example

```php
/**
* Modify category args before validation.
*
* @param array<string,mixed> $args The arguments used to instantiate the category.
* @param string $slug The slug of the category being registered.
*
* @return array<string,mixed> The modified category arguments.
*/
function my_modify_category_args( array $args, string $slug ): array {
if ( 'my-category' === $slug ) {
$args['label'] = __( 'My Custom Label', 'my-plugin' );
$args['description'] = __( 'My custom description for this category.', 'my-plugin' );
}
return $args;
}
add_filter( 'register_ability_category_args', 'my_modify_category_args', 10, 2 );
```
61 changes: 61 additions & 0 deletions docs/7.registering-categories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 7. Registering Categories
Copy link
Member

@gziolo gziolo Oct 13, 2025

Choose a reason for hiding this comment

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

@jonathanbossenger, we really need to remove these numbers, as I would put this one next to registering categories as it fits better there. It's another instance where this ordering causes trouble 😅

Let's also make sure to list this new document in README somwhere next to:

- [Registering Abilities](docs/3.registering-abilities.md)

Copy link
Member

@gziolo gziolo Oct 13, 2025

Choose a reason for hiding this comment

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

Moved the ordering aspect to a new issue here.


Before registering abilities, you must register at least one category. Categories help organize abilities and make them easier to discover and filter.

## Function Signature

```php
wp_register_ability_category( string $slug, array $args ): ?\WP_Ability_Category
```

**Parameters:**
- `$slug` (`string`): A unique identifier for the category. Must contain only lowercase alphanumeric characters and dashes (no underscores, no uppercase).
- `$args` (`array`): Category configuration with these keys:
- `label` (`string`, **Required**): Human-readable name for the category. Should be translatable.
- `description` (`string`, **Required**): Detailed description of the category's purpose. Should be translatable.
- `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the category.

**Return:** (`?\WP_Ability_Category`) An instance of the registered category if it was successfully registered, `null` on failure (e.g., invalid arguments, duplicate slug).

**Note:** Categories must be registered during the `abilities_api_categories_init` action hook.

## Code Example

```php
add_action( 'abilities_api_categories_init', 'my_plugin_register_categories' );
function my_plugin_register_categories() {
wp_register_ability_category( 'data-retrieval', array(
'label' => __( 'Data Retrieval', 'my-plugin' ),
'description' => __( 'Abilities that retrieve and return data from the WordPress site.', 'my-plugin' ),
));

wp_register_ability_category( 'data-modification', array(
'label' => __( 'Data Modification', 'my-plugin' ),
'description' => __( 'Abilities that modify data on the WordPress site.', 'my-plugin' ),
));

wp_register_ability_category( 'communication', array(
'label' => __( 'Communication', 'my-plugin' ),
'description' => __( 'Abilities that send messages or notifications.', 'my-plugin' ),
));
}
```

## Category Slug Convention

The `$slug` parameter must follow these rules:

- **Format:** Must contain only lowercase alphanumeric characters (`a-z`, `0-9`) and hyphens (`-`).
- **Valid examples:** `data-retrieval`, `ecommerce`, `site-information`, `user-management`, `category-123`
- **Invalid examples:**
- Uppercase: `Data-Retrieval`, `MyCategory`
- Underscores: `data_retrieval`
- Special characters: `data.retrieval`, `data/retrieval`, `data retrieval`
- Leading/trailing dashes: `-data`, `data-`
- Double dashes: `data--retrieval`

## Other Category Functions

- `wp_unregister_ability_category( string $slug )` - Remove a registered category. Returns the unregistered category instance or `null` on failure.
- `wp_get_ability_category( string $slug )` - Retrieve a specific category by slug. Returns the category instance or `null` if not found.
- `wp_get_ability_categories()` - Get all registered categories as an associative array keyed by slug.
68 changes: 67 additions & 1 deletion includes/abilities-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@
* prefix, i.e. `my-plugin/my-ability`. It can only contain lowercase
* alphanumeric characters, dashes and the forward slash.
* @param array<string,mixed> $args An associative array of arguments for the ability. This should include
* `label`, `description`, `input_schema`, `output_schema`, `execute_callback`,
* `label`, `description`, `category`, `input_schema`, `output_schema`, `execute_callback`,
* `permission_callback`, `meta`, and `ability_class`.
* @return ?\WP_Ability An instance of registered ability on success, null on failure.
*
* @phpstan-param array{
* label?: string,
* description?: string,
* category?: string,
* execute_callback?: callable( mixed $input= ): (mixed|\WP_Error),
* permission_callback?: callable( mixed $input= ): (bool|\WP_Error),
* input_schema?: array<string,mixed>,
Expand Down Expand Up @@ -102,3 +103,68 @@ function wp_get_ability( string $name ): ?WP_Ability {
function wp_get_abilities(): array {
return WP_Abilities_Registry::get_instance()->get_all_registered();
}

/**
* Registers a new ability category.
*
* @since n.e.x.t
*
* @see WP_Abilities_Category_Registry::register()
*
* @param string $slug The unique slug for the category. Must contain only lowercase
* alphanumeric characters and dashes.
* @param array<string,mixed> $args An associative array of arguments for the category. This should
* include `label`, `description`, and optionally `meta`.
* @return ?\WP_Ability_Category The registered category instance on success, null on failure.
*
* @phpstan-param array{
* label: string,
* description: string,
* meta?: array<string,mixed>,
* ...<string, mixed>
* } $args
*/
function wp_register_ability_category( string $slug, array $args ): ?WP_Ability_Category {
return WP_Abilities_Category_Registry::get_instance()->register( $slug, $args );
}

/**
* Unregisters an ability category.
*
* @since n.e.x.t
*
* @see WP_Abilities_Category_Registry::unregister()
*
* @param string $slug The slug of the registered category.
* @return ?\WP_Ability_Category The unregistered category instance on success, null on failure.
*/
function wp_unregister_ability_category( string $slug ): ?WP_Ability_Category {
return WP_Abilities_Category_Registry::get_instance()->unregister( $slug );
}

/**
* Retrieves a registered ability category.
*
* @since n.e.x.t
*
* @see WP_Abilities_Category_Registry::get_registered()
*
* @param string $slug The slug of the registered category.
* @return ?\WP_Ability_Category The registered category instance, or null if it is not registered.
*/
function wp_get_ability_category( string $slug ): ?WP_Ability_Category {
return WP_Abilities_Category_Registry::get_instance()->get_registered( $slug );
}

/**
* Retrieves all registered ability categories.
*
* @since n.e.x.t
*
* @see WP_Abilities_Category_Registry::get_all_registered()
*
* @return \WP_Ability_Category[] The array of registered categories.
*/
function wp_get_ability_categories(): array {
return WP_Abilities_Category_Registry::get_instance()->get_all_registered();
}
Loading