Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
5 changes: 0 additions & 5 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,8 @@ jobs:
with:
php-version: '8.3'
coverage: none

# Since Composer dependencies are installed using `composer update` and no lock file is in version control,
# passing a custom cache suffix ensures that the cache is flushed at least once per week.
- name: Install Composer dependencies
uses: ramsey/composer-install@a2636af0004d1c0499ffca16ac0b4cc94df70565 # v3.1.0
with:
custom-cache-suffix: $(/bin/date -u --date='last Mon' "+%F")

- name: Setup Node
uses: actions/setup-node@v4
Expand Down
62 changes: 20 additions & 42 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ jobs:
# - Sets up PHP.
# - Configures caching for PHPCS scans.
# - Installs Composer dependencies.
# - Make Composer packages available globally.
# - Runs PHPCS on the full codebase.
# - Generate a report for displaying issues as pull request annotations.
phpcs:
Expand Down Expand Up @@ -68,11 +67,6 @@ jobs:
# passing a custom cache suffix ensures that the cache is flushed at least once per week.
- name: Install Composer dependencies
uses: ramsey/composer-install@a2636af0004d1c0499ffca16ac0b4cc94df70565 # v3.1.0
with:
custom-cache-suffix: ${{ steps.get-date.outputs.date }}

- name: Make Composer packages available globally
run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH"

- name: Run PHPCS
id: phpcs
Expand All @@ -89,12 +83,11 @@ jobs:
# Performs the following steps:
# - Checks out the repository.
# - Sets up PHP.
# - Installs Composer dependencies.
# - Configures caching for PHP static analysis scans.
# - Make Composer packages available globally.
# - Installs Composer dependencies.
# - Makes Composer packages available globally.
# - Runs PHPStan static analysis (with Pull Request annotations).
# - Saves the PHPStan result cache.
# - Ensures version-controlled files are not modified or deleted.
phpstan:
name: Run PHP static analysis
runs-on: ubuntu-24.04
Expand All @@ -116,30 +109,26 @@ jobs:
coverage: none
tools: cs2pr

# This date is used to ensure that the Composer cache is cleared at least once every week.
# This date is used to ensure that the PHPCS cache is cleared at least once every week.
# http://man7.org/linux/man-pages/man1/date.1.html
- name: "Get last Monday's date"
id: get-date
run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT"

# Since Composer dependencies are installed using `composer update` and no lock file is in version control,
# passing a custom cache suffix ensures that the cache is flushed at least once per week.
- name: Install Composer dependencies
uses: ramsey/composer-install@a2636af0004d1c0499ffca16ac0b4cc94df70565 # v3.1.0
with:
custom-cache-suffix: ${{ steps.get-date.outputs.date }}

- name: Make Composer packages available globally
run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH"

- name: Cache PHP Static Analysis scan cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: tests/_output # This is defined in the base.neon file.
key: 'phpstan-result-cache-${{ github.run_id }}'
key: 'phpstan-result-cache-${{ runner.os }}-date-${{ steps.get-date.outputs.date }}'
restore-keys: |
phpstan-result-cache-

- name: Install Composer dependencies
uses: ramsey/composer-install@a2636af0004d1c0499ffca16ac0b4cc94df70565 # v3.1.0

- name: Make Composer packages available globally
run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH"

- name: Run PHP static analysis tests
id: phpstan
run: phpstan analyse -vvv --error-format=checkstyle | cs2pr
Expand All @@ -149,36 +138,30 @@ jobs:
if: ${{ !cancelled() }}
with:
path: tests/_output
key: 'phpstan-result-cache-${{ github.run_id }}'
key: 'phpstan-result-cache-${{ runner.os }}-date-${{ steps.get-date.outputs.date }}'

# Runs the PHPUnit tests for WordPress.
#
# Performs the following steps:
# - Sets environment variables.
# - Checks out the repository.
# - Sets up Node.js.
# - Sets up PHP.
# - Installs Composer dependencies.
# - Installs npm dependencies
# - Logs general debug information about the runner.
# - Logs Docker debug information (about the Docker installation within the runner).
# - Starts the WordPress Docker container.
# - Logs the running Docker containers.
# - Logs debug information about what's installed within the WordPress Docker containers.
# - Install WordPress within the Docker container.
# - Run the PHPUnit tests.
# - Upload the code coverage report to Codecov.io.
# - Upload the HTML code coverage report as an artifact.
# - Ensures version-controlled files are not modified or deleted.
# - Checks out the WordPress Test reporter repository.
# - Submit the test results to the WordPress.org host test results.
# - Sets up Node.js.
# - Installs npm dependencies.
# - Starts the WordPress Docker testing environment (with or without Xdebug coverage).
# - Logs PHP and WordPress versions from the container.
# - Runs PHPUnit tests (with coverage if enabled).
# - Uploads code coverage report to Codecov.io (if coverage is enabled).
# - Uploads HTML coverage report as an artifact (if coverage is enabled).
phpunit:
name: Test PHP ${{ matrix.php }} WP ${{ matrix.wp }}${{ matrix.coverage && ' with coverage' || '' }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.3', '8.2', '8.1', '8.0', '7.4']
wp: [latest, trunk ]
wp: [latest, trunk]
coverage: [false]
include:
- php: '8.4'
Expand Down Expand Up @@ -213,12 +196,8 @@ jobs:
php-version: '${{ matrix.php }}'
coverage: none

# Since Composer dependencies are installed using `composer update` and no lock file is in version control,
# passing a custom cache suffix ensures that the cache is flushed at least once per week.
- name: Install Composer dependencies
uses: ramsey/composer-install@a2636af0004d1c0499ffca16ac0b4cc94df70565 # v3.1.0
with:
custom-cache-suffix: $(/bin/date -u --date='last Mon' "+%F")

- name: Setup Node
uses: actions/setup-node@v4
Expand Down Expand Up @@ -247,7 +226,6 @@ jobs:
npm run wp-env -- run cli wp core version

- name: Run PHPUnit tests${{ matrix.coverage && ' with coverage report' || '' }}
continue-on-error: true
id: phpunit
run: |
npm run test:php
Expand Down
11 changes: 11 additions & 0 deletions includes/abilities-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@
* include `label`, `description`, `input_schema`, `output_schema`,
* `execute_callback`, `permission_callback`, and `meta`.
* @return ?\WP_Ability An instance of registered ability on success, null on failure.
*
* @phpstan-param array{
* label?: string,
* description?: string,
* input_schema?: array<string,mixed>,
* output_schema?: array<string,mixed>,
* execute_callback?: callable( array<string,mixed> $input): (mixed|\WP_Error),
* permission_callback?: callable( ?array<string,mixed> $input ): bool,
* meta?: array<string,mixed>,
* ...<string, mixed>
* } $properties
*/
function wp_register_ability( $name, array $properties = array() ): ?WP_Ability {
if ( ! did_action( 'abilities_api_init' ) ) {
Expand Down
29 changes: 23 additions & 6 deletions includes/abilities-api/class-wp-abilities-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
* @access private
*/
final class WP_Abilities_Registry {
/**
* The singleton instance of the registry.
*
* @since 0.1.0
* @var ?self
*/
private static $instance = null;

/**
* Holds the registered abilities.
*
Expand All @@ -42,6 +50,17 @@ final class WP_Abilities_Registry {
* include `label`, `description`, `input_schema`, `output_schema`,
* `execute_callback`, `permission_callback`, and `meta`.
* @return ?\WP_Ability The registered ability instance on success, null on failure.
*
* @phpstan-param array{
* label?: string,
* description?: string,
* input_schema?: array<string,mixed>,
* output_schema?: array<string,mixed>,
* execute_callback?: callable( array<string,mixed> $input): (mixed|\WP_Error),
* permission_callback?: ?callable( ?array<string,mixed> $input ): bool,
* meta?: array<string,mixed>,
* ...<string, mixed>
* } $properties
*/
public function register( $name, array $properties = array() ): ?WP_Ability {
$ability = null;
Expand Down Expand Up @@ -248,11 +267,9 @@ public function get_registered( string $name ): ?WP_Ability {
* @return \WP_Abilities_Registry The main registry instance.
*/
public static function get_instance(): self {
/** @var \WP_Abilities_Registry $wp_abilities */
global $wp_abilities;
if ( null === self::$instance ) {
self::$instance = new self();

if ( empty( $wp_abilities ) ) {
$wp_abilities = new self();
/**
* Fires when preparing abilities registry.
*
Expand All @@ -263,10 +280,10 @@ public static function get_instance(): self {
*
* @param \WP_Abilities_Registry $instance Abilities registry object.
*/
do_action( 'abilities_api_init', $wp_abilities );
do_action( 'abilities_api_init', self::$instance );
}

return $wp_abilities;
return self::$instance;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions includes/abilities-api/class-wp-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ class WP_Ability {
* @param array<string,mixed> $properties An associative array of properties for the ability. This should
* include `label`, `description`, `input_schema`, `output_schema`,
* `execute_callback`, `permission_callback`, and `meta`.
*
* @phpstan-param array{
* label: string,
* description: string,
* input_schema?: array<string,mixed>,
* output_schema?: array<string,mixed>,
* execute_callback: callable( array<string,mixed> $input): (mixed|\WP_Error),
* permission_callback?: ?callable( ?array<string,mixed> $input ): bool,
* meta?: array<string,mixed>,
* } $properties
*/
public function __construct( string $name, array $properties ) {
$this->name = $name;
Expand Down
31 changes: 17 additions & 14 deletions tests/unit/abilities-api/wpRegisterAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ public function set_up(): void {
'description' => 'The result of adding the two numbers.',
'required' => true,
),
'execute_callback' => function ( array $input ): int {
'execute_callback' => static function ( array $input ): int {
return $input['a'] + $input['b'];
},
'permission_callback' => function (): bool {
'permission_callback' => static function (): bool {
return true;
},
'meta' => array(
Expand All @@ -60,9 +60,11 @@ public function set_up(): void {
*/
public function tear_down(): void {
foreach ( wp_get_abilities() as $ability ) {
if ( str_starts_with( $ability->get_name(), 'test/' ) ) {
wp_unregister_ability( $ability->get_name() );
if ( ! str_starts_with( $ability->get_name(), 'test/' ) ) {
continue;
}

wp_unregister_ability( $ability->get_name() );
}

parent::tear_down();
Expand Down Expand Up @@ -147,7 +149,7 @@ public function test_register_valid_ability(): void {
public function test_register_ability_no_permissions(): void {
do_action( 'abilities_api_init' );

self::$test_ability_properties['permission_callback'] = function (): bool {
self::$test_ability_properties['permission_callback'] = static function (): bool {
return false;
};
$result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties );
Expand Down Expand Up @@ -200,7 +202,7 @@ public function test_execute_ability_no_input_schema_match(): void {
public function test_execute_ability_no_output_schema_match(): void {
do_action( 'abilities_api_init' );

self::$test_ability_properties['execute_callback'] = function (): bool {
self::$test_ability_properties['execute_callback'] = static function (): bool {
return true;
};
$result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties );
Expand Down Expand Up @@ -243,7 +245,7 @@ public function test_permission_callback_receives_input(): void {
do_action( 'abilities_api_init' );

$received_input = null;
self::$test_ability_properties['permission_callback'] = function ( array $input ) use ( &$received_input ): bool {
self::$test_ability_properties['permission_callback'] = static function ( array $input ) use ( &$received_input ): bool {
$received_input = $input;
// Allow only if 'a' is greater than 'b'
return $input['a'] > $input['b'];
Expand Down Expand Up @@ -310,25 +312,26 @@ public function test_get_existing_ability() {

$name = self::$test_ability_name;
$properties = self::$test_ability_properties;
$callback = function ( $instance ) use ( $name, $properties ) {
$callback = static function ( $instance ) use ( $name, $properties ) {
wp_register_ability( $name, $properties );
};

add_action( 'abilities_api_init', $callback );

// Temporarily set `$wp_abilities` to null to ensure `wp_get_ability()` triggers `abilities_api_init` action.
$old_wp_abilities = $wp_abilities;
$wp_abilities = null;
// Reset the Registry, to ensure it's empty before the test.
$registry_reflection = new ReflectionClass( WP_Abilities_Registry::class );
$instance_prop = $registry_reflection->getProperty( 'instance' );
$instance_prop->setAccessible( true );
$instance_prop->setValue( null );

$result = wp_get_ability( $name );

$wp_abilities = $old_wp_abilities;

remove_action( 'abilities_api_init', $callback );

$this->assertEquals(
new WP_Ability( $name, $properties ),
$result
$result,
'Ability does not share expected properties.'
);
}

Expand Down
Loading