Skip to content

feat: add AbstractAbility and AbstractAbilityRegistrar - #76

Open
Adi-ty wants to merge 4 commits into
mainfrom
feat/ability-abstracts
Open

feat: add AbstractAbility and AbstractAbilityRegistrar#76
Adi-ty wants to merge 4 commits into
mainfrom
feat/ability-abstracts

Conversation

@Adi-ty

@Adi-ty Adi-ty commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Add AbstractAbility and AbstractAbilityRegistrar — telemetry-agnostic base classes for the WordPress 6.9+ Abilities API. The telemetry-shaped AbstractAbility from wp-dev-tools is decomposed: registration timing and category ownership live on the registrar, the ability stays a plain describable object. Both are hook-guarded, so the package's 6.5 floor is unchanged.

Closes

Part of rtcamp/wp-devtools#12

Changes

  • inc/Contracts/Abstracts/AbstractAbility.php — declarative base with name(), label(), description(), category(), input_schema(), output_schema(), execute(); overridable permission() (default: manage_options, fail-closed) and meta() (default: [], exposure opt-in); args() maps to wp_register_ability() shape, omitting empty schemas/meta
  • inc/Contracts/Abstracts/AbstractAbilityRegistrar.php — implements Registrable; attaches to wp_abilities_api_categories_init (category) and wp_abilities_api_init (abilities); abstract category_slug(), category_description(), abilities(); overridable category_label() (title-cases slug), before_register() hook; category registration idempotent across registrars
  • tests/Contracts/Abstracts/AbstractAbilityTest.php — 10 tests covering arg mapping, schema omission, meta default/propagation, permission tiers, permission override, input pass-through; pure logic, runs on all WP versions
  • Docs — README (10→12 abstracts), docs/abstracts.md (new "Abilities" cookbook with full example), docs/index.md (twelve)
  • AI instructions — ai/framework-php.instructions.md: add Feature/Ability/AbilityRegistrar to abstracts brace list, add wp_register_ability to direct-call rule 5; .github/instructions/php.instructions.md: add all three to abstracts list; AGENTS.md: note Abilities API guard

How I verified

❯ composer lint
DEPRECATED: Scanning CSS/JS files is deprecated and support will be removed in PHP_CodeSniffer 4.0.
The WordPressVIPMinimum.JS.Window sniff is listening for JS.
DEPRECATED: Scanning CSS/JS files is deprecated and support will be removed in PHP_CodeSniffer 4.0.
The WordPressVIPMinimum.JS.DangerouslySetInnerHTML sniff is listening for JS.
DEPRECATED: Scanning CSS/JS files is deprecated and support will be removed in PHP_CodeSniffer 4.0.
The WordPressVIPMinimum.JS.InnerHTML sniff is listening for JS.
DEPRECATED: Scanning CSS/JS files is deprecated and support will be removed in PHP_CodeSniffer 4.0.
The WordPressVIPMinimum.JS.StrippingTags sniff is listening for JS.
DEPRECATED: Scanning CSS/JS files is deprecated and support will be removed in PHP_CodeSniffer 4.0.
The WordPressVIPMinimum.JS.StringConcat sniff is listening for JS.
DEPRECATED: Scanning CSS/JS files is deprecated and support will be removed in PHP_CodeSniffer 4.0.
The WordPressVIPMinimum.JS.HTMLExecutingFunctions sniff is listening for JS.

........ 8 / 8 (100%)


Time: 582ms; Memory: 16MB

❯ composer analyse
Note: Using configuration file /Users/adi/rtproj/wp-php-toolkit/phpstan.neon.dist.
 30/30 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


                                                                                                                        
 [OK] No errors                                                                                                         
                                                                                                                        

❯ npm run test:php

> wp-framework@0.0.1 pretest:php
> wp-env run tests-cli --env-cwd=wp-content/wp-framework-rt -- composer install --no-interaction

ℹ Starting 'composer install --no-interaction' on the tests-cli container. 

Composer could not detect the root package (rtcamp/wp-framework) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Nothing to install, update or remove
Generating optimized autoload files
Class rtCamp\WPFramework\Tests\Fixtures\LoaderRunner located in ./tests/Fixtures/LoaderFixtures.php does not comply with psr-4 autoloading standard (rule: rtCamp\WPFramework\Tests\ => ./tests). Skipping.
Class rtCamp\WPFramework\Tests\Fixtures\PlainRegistrable located in ./tests/Fixtures/LoaderFixtures.php does not comply with psr-4 autoloading standard (rule: rtCamp\WPFramework\Tests\ => ./tests). Skipping.
Class rtCamp\WPFramework\Tests\Fixtures\ConditionalAllowed located in ./tests/Fixtures/LoaderFixtures.php does not comply with psr-4 autoloading standard (rule: rtCamp\WPFramework\Tests\ => ./tests). Skipping.
Class rtCamp\WPFramework\Tests\Fixtures\ConditionalDenied located in ./tests/Fixtures/LoaderFixtures.php does not comply with psr-4 autoloading standard (rule: rtCamp\WPFramework\Tests\ => ./tests). Skipping.
Class rtCamp\WPFramework\Tests\Fixtures\ShareableRegistrable located in ./tests/Fixtures/LoaderFixtures.php does not comply with psr-4 autoloading standard (rule: rtCamp\WPFramework\Tests\ => ./tests). Skipping.
34 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
✔ Ran `composer install --no-interaction` in 'tests-cli'. (in 2s 912ms)

> wp-framework@0.0.1 test:php
> wp-env run tests-cli --env-cwd=wp-content/wp-framework-rt -- vendor/bin/phpunit -c phpunit.xml.dist

ℹ Starting 'vendor/bin/phpunit -c phpunit.xml.dist' on the tests-cli container. 

Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
Not running ajax tests. To execute these, use --group ajax.
Not running ms-files tests. To execute these, use --group ms-files.
Not running external-http tests. To execute these, use --group external-http.
PHPUnit 9.6.34 by Sebastian Bergmann and contributors.

Random Seed:   1784796014
Warning:       No code coverage driver available

...............................................................  63 / 238 ( 26%)
............................................................... 126 / 238 ( 52%)
............................................................... 189 / 238 ( 79%)
.................................................               238 / 238 (100%)

Time: 00:00.614, Memory: 48.50 MB

OK (238 tests, 487 assertions)
✔ Ran `vendor/bin/phpunit -c phpunit.xml.dist` in 'tests-cli'. (in 2s 929ms)

Acceptance criteria

Runtime behavior

  • AbstractAbility and AbstractAbilityRegistrar exist in wp-framework with unit tests; a trivial ability can be defined and registers on wp_abilities_api_init.
  • Abilities round-trip via wp_get_ability() with correct label/category
  • On WP < 6.9: all registrar tests skip, ability tests run (pure logic)

Code quality

  • Lint — zero errors
  • Static analysis — zero errors
  • Tests pass

Reviewer notes

  • AbstractAbility does not implement Registrable — abilities may only be registered inside the Abilities API init hooks, so the paired registrar owns that timing. This diverges from the usual Abstract* pattern but is called out in the class docblock and the cookbook.

@Adi-ty
Adi-ty marked this pull request as ready for review July 23, 2026 08:45
Copilot AI balanced review requested due to automatic review settings July 23, 2026 08:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds WordPress Abilities API abstractions, registration support, tests, and consumer guidance.

Changes:

  • Introduces AbstractAbility and AbstractAbilityRegistrar.
  • Adds unit and integration coverage for mapping, permissions, registration, and execution.
  • Updates framework documentation and AI instructions.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
inc/Contracts/Abstracts/AbstractAbility.php Defines the ability contract and registration arguments.
inc/Contracts/Abstracts/AbstractAbilityRegistrar.php Registers categories and abilities on API hooks.
tests/Contracts/Abstracts/AbstractAbilityTest.php Tests argument mapping and permissions.
tests/Contracts/Abstracts/AbstractAbilityRegistrarTest.php Tests registration and execution round-trips.
README.md Lists the new abstracts.
docs/index.md Updates abstract counts and navigation.
docs/abstracts.md Adds the Abilities API cookbook.
ai/framework-php.instructions.md Adds consumer review guidance.
.github/instructions/php.instructions.md Updates the framework abstract inventory.
AGENTS.md Documents WordPress-version compatibility.

Comment thread inc/Contracts/Abstracts/AbstractAbility.php Outdated
@Adi-ty Adi-ty changed the title Feat/ability abstracts feat: add AbstractAbility and AbstractAbilityRegistrar Jul 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (3)

inc/Contracts/Abstracts/AbstractAbilityRegistrar.php:54

  • The docblock says the function_exists guards are for partial backports, but register_category() only guards wp_register_ability_category() and then unconditionally calls wp_has_ability_category(). On a partial backport where wp_register_ability_category() exists but wp_has_ability_category() does not, this will fatal. Consider guarding wp_has_ability_category() as well (or using a more defensive check that doesn’t require it).
	public function register_category(): void {
		if ( ! function_exists( 'wp_register_ability_category' ) ) {
			return;
		}

		if ( wp_has_ability_category( $this->category_slug() ) ) {
			return;
		}

inc/Contracts/Abstracts/AbstractAbility.php:7

  • @since 1.0.0 appears inconsistent with the rest of this PR (tests are annotated @since 0.0.1, and the repo/package versioning in the PR metadata suggests this is part of the current pre-1.0 series). If 1.0.0 is not the intended release version for this addition, align @since tags to the correct package version to avoid misleading generated docs/changelogs.
/**
 * Abstract Ability.
 *
 * @package rtCamp\WPFramework\Contracts\Abstracts
 * @since   1.0.0
 */

inc/Contracts/Abstracts/AbstractAbilityRegistrar.php:77

  • Because abilities() is typed as array at the signature level, an accidental non-AbstractAbility value will cause a hard fatal when calling name()/args(). Consider adding a lightweight runtime validation (e.g., skip/throw with a clear message) to make contract violations easier to diagnose in consumer code.
		foreach ( $this->abilities() as $ability ) {
			wp_register_ability( $ability->name(), $ability->args() );
		}

Adi-ty added 3 commits August 4, 2026 13:54
Telemetry-agnostic base classes for the WordPress Abilities API (6.9+),
lifted from the shape proven in rtcamp/wp-dev-tools (rtCamp/wp-devtools#12,
item 1). AbstractAbility declares the pieces of an ability and maps them to
wp_register_ability() args; AbstractAbilityRegistrar (a Registrable) hooks
wp_abilities_api_categories_init / wp_abilities_api_init, registers the
shared category idempotently, and loops its abilities.

Both callbacks are wrapped in defaulted closures because core invokes them
with zero arguments when no input schema is declared. category_description()
is abstract: core rejects categories without a non-empty description. On
cores older than 6.9 the hooks never fire, so the registrar is inert and the
package's WordPress floor is unchanged.
Abstracts cookbook gains an Abilities section (must-implement list, override
seams, shared-category consumer example, the WP <6.9 inertness note); README
and docs index updated. The AI-instruction ability lists now name the ability
abstracts and wp_register_ability, and pick up AbstractFeature, which was
missing from both since it landed.
Copilot AI review requested due to automatic review settings August 4, 2026 08:24
@Adi-ty
Adi-ty force-pushed the feat/ability-abstracts branch from 90bfbd3 to 55ade71 Compare August 4, 2026 08:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

inc/Contracts/Abstracts/AbstractAbilityRegistrar.php:54

  • This method guards wp_register_ability_category() but still calls wp_has_ability_category() unconditionally. If the Abilities API is partially available (which the docblock explicitly mentions), this can fatal when wp_has_ability_category() is missing. Suggest guarding the existence of all Abilities API functions used here (or using a single capability check that implies all required functions are present) before calling them.
	public function register_category(): void {
		if ( ! function_exists( 'wp_register_ability_category' ) ) {
			return;
		}

		if ( wp_has_ability_category( $this->category_slug() ) ) {
			return;
		}

inc/Contracts/Abstracts/AbstractAbilityRegistrar.php:42

  • Both callbacks (register_category() / register_abilities()) declare zero parameters, but add_action() defaults $accepted_args to 1. Setting $accepted_args to 0 here avoids accidental ArgumentCountError if the core hooks ever pass an argument (and documents the intended signature).
	public function register_hooks(): void {
		add_action( 'wp_abilities_api_categories_init', [ $this, 'register_category' ] );
		add_action( 'wp_abilities_api_init', [ $this, 'register_abilities' ] );
	}

inc/Contracts/Abstracts/AbstractAbility.php:6

  • The @since tag is set to 1.0.0, but the PR/testing context indicates the package version is still 0.0.1. Please align @since to the actual version this abstraction is being introduced in (and keep it consistent with the surrounding codebase/doc conventions).
 * @package rtCamp\WPFramework\Contracts\Abstracts
 * @since   1.0.0

inc/Contracts/Abstracts/AbstractAbilityRegistrar.php:6

  • Same as AbstractAbility.php: the @since tag is 1.0.0 but appears inconsistent with the current package version in this repo/tests. Please update @since to the actual introduction version and keep the two new abstractions consistent.
 * @package rtCamp\WPFramework\Contracts\Abstracts
 * @since   1.0.0

Comment thread tests/Contracts/Abstracts/AbstractAbilityRegistrarTest.php
Copilot AI review requested due to automatic review settings August 5, 2026 06:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (3)

inc/Contracts/Abstracts/AbstractAbilityRegistrar.php:54

  • register_category() guards wp_register_ability_category() but calls wp_has_ability_category() unguarded, which can fatally error in “partial backport” scenarios (the docblock explicitly mentions these). Consider guarding wp_has_ability_category() with function_exists() and falling back to unconditional registration when it’s missing (or skipping the idempotency check in that case).
	public function register_category(): void {
		if ( ! function_exists( 'wp_register_ability_category' ) ) {
			return;
		}

		if ( wp_has_ability_category( $this->category_slug() ) ) {
			return;
		}

inc/Contracts/Abstracts/AbstractAbility.php:6

  • The new classes are tagged @since 1.0.0, but the new test classes are tagged @since 0.0.1 and the PR context indicates the package is currently 0.0.1. Please align the @since tags to the actual first release version that will ship these classes (or update the rest of the repo docs/tests if 1.0.0 is correct).
 * @package rtCamp\WPFramework\Contracts\Abstracts
 * @since   1.0.0

tests/Contracts/Abstracts/AbstractAbilityTest.php:116

  • This test asserts the default permission callback returns false but doesn’t set the current user. If the test suite (or TestCase) doesn’t reliably reset the current user between tests, this can become order-dependent/flaky. Consider explicitly calling wp_set_current_user( 0 ) (or otherwise setting a known user) within this test before asserting false.
	public function test_callbacks_are_safe_to_invoke_without_arguments(): void {
		// Core invokes both callbacks with zero arguments when the ability
		// declares no input schema; the closure wrappers absorb that.
		$args = $this->make_ability()->args();

		$this->assertSame( [ 'received' => null ], $args['execute_callback']() );
		$this->assertFalse( $args['permission_callback']() );
	}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants