From 3982ca4bedc8d8277c4d20e243940bd22e444613 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Tue, 2 Sep 2025 18:53:26 +0300 Subject: [PATCH] dev!: handle property registration inside WP_Ability. --- .../class-wp-abilities-registry.php | 78 +++-------------- includes/abilities-api/class-wp-ability.php | 83 ++++++++++++++++--- .../abilities-api/wpAbilitiesRegistry.php | 18 ++++ 3 files changed, 102 insertions(+), 77 deletions(-) diff --git a/includes/abilities-api/class-wp-abilities-registry.php b/includes/abilities-api/class-wp-abilities-registry.php index e36decd4..c0505a05 100644 --- a/includes/abilities-api/class-wp-abilities-registry.php +++ b/includes/abilities-api/class-wp-abilities-registry.php @@ -85,87 +85,33 @@ public function register( string $name, array $properties = array() ): ?WP_Abili return null; } - if ( empty( $properties['label'] ) || ! is_string( $properties['label'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties must contain a `label` string.' ), - '0.1.0' - ); - return null; - } - - if ( empty( $properties['description'] ) || ! is_string( $properties['description'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties must contain a `description` string.' ), - '0.1.0' - ); - return null; - } - - if ( isset( $properties['input_schema'] ) && ! is_array( $properties['input_schema'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties should provide a valid `input_schema` definition.' ), - '0.1.0' - ); - return null; - } - - if ( isset( $properties['output_schema'] ) && ! is_array( $properties['output_schema'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties should provide a valid `output_schema` definition.' ), - '0.1.0' - ); - return null; - } - - if ( empty( $properties['execute_callback'] ) || ! is_callable( $properties['execute_callback'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties must contain a valid `execute_callback` function.' ), - '0.1.0' - ); - return null; - } - - if ( isset( $properties['permission_callback'] ) && ! is_callable( $properties['permission_callback'] ) ) { + // The class is only used to instantiate the ability, and is not a property of the ability itself. + if ( isset( $properties['ability_class'] ) && ! is_a( $properties['ability_class'], WP_Ability::class, true ) ) { _doing_it_wrong( __METHOD__, - esc_html__( 'The ability properties should provide a valid `permission_callback` function.' ), + esc_html__( 'The ability properties should provide a valid `ability_class` that extends WP_Ability.' ), '0.1.0' ); return null; } + $ability_class = $properties['ability_class'] ?? WP_Ability::class; + unset( $properties['ability_class'] ); - if ( isset( $properties['meta'] ) && ! is_array( $properties['meta'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties should provide a valid `meta` array.' ), - '0.1.0' + try { + // WP_Ability::validate_properties() will throw an exception if the properties are invalid. + $ability = new $ability_class( + $name, + $properties ); - return null; - } - - if ( isset( $properties['ability_class'] ) && ! is_a( $properties['ability_class'], WP_Ability::class, true ) ) { + } catch ( \InvalidArgumentException $e ) { _doing_it_wrong( __METHOD__, - esc_html__( 'The ability properties should provide a valid `ability_class` that extends WP_Ability.' ), + esc_html( $e->getMessage() ), '0.1.0' ); return null; } - // The class is only used to instantiate the ability, and is not a property of the ability itself. - $ability_class = $properties['ability_class'] ?? WP_Ability::class; - unset( $properties['ability_class'] ); - - $ability = new $ability_class( - $name, - $properties - ); - $this->registered_abilities[ $name ] = $ability; return $ability; } diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index c55eabf6..11b0affc 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -100,21 +100,12 @@ 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|\WP_Error), - * meta?: array, - * ..., - * } $properties */ public function __construct( string $name, array $properties ) { $this->name = $name; + $this->validate_properties( $properties ); + foreach ( $properties as $property_name => $property_value ) { if ( ! property_exists( $this, $property_name ) ) { _doing_it_wrong( @@ -202,6 +193,76 @@ public function get_meta(): array { return $this->meta; } + /** + * Validates the properties used to instantiate the ability. + * + * Errors are thrown as exceptions instead of \WP_Errors to allow for simpler handling and overloading. They are then + * caught and converted to a WP_Error when by WP_Abilities_Registry::register(). + * + * @since n.e.x.t + * + * @see WP_Abilities_Registry::register() + * + * @param array $properties An associative array of properties to validate. + * + * @return void + * @throws \InvalidArgumentException if the properties are invalid. + * + * @phpstan-assert 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|\WP_Error), + * meta?: array, + * ..., + * } $properties + */ + protected function validate_properties( array $properties ) { + if ( empty( $properties['label'] ) || ! is_string( $properties['label'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties must contain a `label` string.' ) + ); + } + + if ( empty( $properties['description'] ) || ! is_string( $properties['description'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties must contain a `description` string.' ) + ); + } + + if ( isset( $properties['input_schema'] ) && ! is_array( $properties['input_schema'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties should provide a valid `input_schema` definition.' ) + ); + } + + if ( isset( $properties['output_schema'] ) && ! is_array( $properties['output_schema'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties should provide a valid `output_schema` definition.' ) + ); + } + + if ( empty( $properties['execute_callback'] ) || ! is_callable( $properties['execute_callback'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties must contain a valid `execute_callback` function.' ) + ); + } + + if ( isset( $properties['permission_callback'] ) && ! is_callable( $properties['permission_callback'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties should provide a valid `permission_callback` function.' ) + ); + } + + if ( isset( $properties['meta'] ) && ! is_array( $properties['meta'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties should provide a valid `meta` array.' ) + ); + } + } + /** * Validates input data against the input schema. * diff --git a/tests/unit/abilities-api/wpAbilitiesRegistry.php b/tests/unit/abilities-api/wpAbilitiesRegistry.php index dff53e97..01e5ebc1 100644 --- a/tests/unit/abilities-api/wpAbilitiesRegistry.php +++ b/tests/unit/abilities-api/wpAbilitiesRegistry.php @@ -383,4 +383,22 @@ public function test_get_all_registered() { $this->assertSame( $ability_two_name, $result[ $ability_two_name ]->get_name() ); $this->assertSame( $ability_three_name, $result[ $ability_three_name ]->get_name() ); } + + /** + * Direct instantiation of WP_Ability with invalid properties should throw an exception. + * + * @covers WP_Ability::__construct + * @covers WP_Ability::validate_properties + */ + public function test_wp_ability_invalid_properties_throws_exception() { + $this->expectException( \InvalidArgumentException::class ); + new WP_Ability( + 'test/invalid', + array( + 'label' => '', + 'description' => '', + 'execute_callback' => null, + ) + ); + } }