diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index a737a922..37f1e54f 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c4a9861..9a339eb3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: @@ -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 @@ -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 @@ -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 @@ -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' @@ -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 @@ -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 diff --git a/includes/abilities-api.php b/includes/abilities-api.php index ae538a2a..3839670b 100644 --- a/includes/abilities-api.php +++ b/includes/abilities-api.php @@ -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, + * output_schema?: array, + * execute_callback?: callable( array $input): (mixed|\WP_Error), + * permission_callback?: callable( ?array $input ): bool, + * meta?: array, + * ... + * } $properties */ function wp_register_ability( $name, array $properties = array() ): ?WP_Ability { if ( ! did_action( 'abilities_api_init' ) ) { diff --git a/includes/abilities-api/class-wp-abilities-registry.php b/includes/abilities-api/class-wp-abilities-registry.php index 0e72925a..8ec5867e 100644 --- a/includes/abilities-api/class-wp-abilities-registry.php +++ b/includes/abilities-api/class-wp-abilities-registry.php @@ -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. * @@ -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, + * output_schema?: array, + * execute_callback?: callable( array $input): (mixed|\WP_Error), + * permission_callback?: ?callable( ?array $input ): bool, + * meta?: array, + * ... + * } $properties */ public function register( $name, array $properties = array() ): ?WP_Ability { $ability = null; @@ -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. * @@ -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; } /** diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index f5ee8fbb..473a2b3b 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -99,6 +99,16 @@ class WP_Ability { * @param array $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, + * output_schema?: array, + * execute_callback: callable( array $input): (mixed|\WP_Error), + * permission_callback?: ?callable( ?array $input ): bool, + * meta?: array, + * } $properties */ public function __construct( string $name, array $properties ) { $this->name = $name; diff --git a/tests/unit/abilities-api/wpRegisterAbility.php b/tests/unit/abilities-api/wpRegisterAbility.php index e3709a12..f1337aba 100644 --- a/tests/unit/abilities-api/wpRegisterAbility.php +++ b/tests/unit/abilities-api/wpRegisterAbility.php @@ -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( @@ -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(); @@ -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 ); @@ -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 ); @@ -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']; @@ -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.' ); }