Skip to content

Commit e8d70f7

Browse files
committed
Abilities API: Implement server-side registry
1 parent 7007081 commit e8d70f7

File tree

6 files changed

+1410
-0
lines changed

6 files changed

+1410
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php declare( strict_types = 1 );
2+
3+
/**
4+
* Abilities API
5+
*
6+
* Defines functions for managing abilities in WordPress.
7+
*
8+
* @package WordPress
9+
* @subpackage Abilities API
10+
* @since 0.1.0
11+
*/
12+
13+
/**
14+
* Registers a new ability using Abilities API.
15+
*
16+
* Note: Do not use before the {@see 'abilities_api_init'} hook.
17+
*
18+
* @see WP_Abilities_Registry::register()
19+
*
20+
* @since 0.1.0
21+
*
22+
* @param string|WP_Ability $name The name of the ability, or WP_Ability instance. The name must be a string
23+
* containing a namespace prefix, i.e. `my-plugin/my-ability`. It can only
24+
* contain lowercase alphanumeric characters, dashes and the forward slash.
25+
* @param array $properties Optional. An associative array of properties for the ability. This should
26+
* include `label`, `description`, `input_schema`, `output_schema`,
27+
* `execute_callback`, `permission_callback`, and `meta`.
28+
* @return ?WP_Ability An instance of registered ability on success, null on failure.
29+
*/
30+
function wp_register_ability( $name, array $properties = array() ): ?WP_Ability {
31+
if ( ! did_action( 'abilities_api_init' ) ) {
32+
_doing_it_wrong(
33+
__FUNCTION__,
34+
sprintf(
35+
/* translators: 1: abilities_api_init, 2: string value of the ability name. */
36+
esc_html__( 'Abilities must be registered on the %1$s action. The ability %2$s was not registered.' ),
37+
'<code>abilities_api_init</code>',
38+
'<code>' . esc_attr( $name ) . '</code>'
39+
),
40+
'0.1.0'
41+
);
42+
return null;
43+
}
44+
45+
return WP_Abilities_Registry::get_instance()->register( $name, $properties );
46+
}
47+
48+
/**
49+
* Unregisters an ability using Abilities API.
50+
*
51+
* @see WP_Abilities_Registry::unregister()
52+
*
53+
* @since 0.1.0
54+
*
55+
* @param string $name The name of the registered ability, with its namespace.
56+
* @return ?WP_Ability The unregistered ability instance on success, null on failure.
57+
*/
58+
function wp_unregister_ability( string $name ): ?WP_Ability {
59+
return WP_Abilities_Registry::get_instance()->unregister( $name );
60+
}
61+
62+
/**
63+
* Retrieves a registered ability using Abilities API.
64+
*
65+
* @see WP_Abilities_Registry::get_registered()
66+
*
67+
* @since 0.1.0
68+
*
69+
* @param string $name The name of the registered ability, with its namespace.
70+
* @return ?WP_Ability The registered ability instance, or null if it is not registered.
71+
*/
72+
function wp_get_ability( string $name ): ?WP_Ability {
73+
return WP_Abilities_Registry::get_instance()->get_registered( $name );
74+
}
75+
76+
/**
77+
* Retrieves all registered abilities using Abilities API.
78+
*
79+
* @see WP_Abilities_Registry::get_all_registered()
80+
*
81+
* @since 0.1.0
82+
*
83+
* @return WP_Ability[] The array of registered abilities.
84+
*/
85+
function wp_get_abilities(): array {
86+
return WP_Abilities_Registry::get_instance()->get_all_registered();
87+
}
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
<?php declare( strict_types = 1 );
2+
3+
/**
4+
* Abilities API
5+
*
6+
* Defines WP_Abilities_Registry class.
7+
*
8+
* @package WordPress
9+
* @subpackage Abilities API
10+
* @since 0.1.0
11+
*/
12+
13+
/**
14+
* Manages the registration and lookup of abilities.
15+
*
16+
* @since 0.1.0
17+
* @access private
18+
*/
19+
final class WP_Abilities_Registry {
20+
/**
21+
* Holds the registered abilities.
22+
*
23+
* @since 0.1.0
24+
* @var WP_Ability[]
25+
*/
26+
private array $registered_abilities = [];
27+
28+
/**
29+
* Container for the main instance of the class.
30+
*
31+
* @since 0.1.0
32+
* @var ?WP_Abilities_Registry
33+
*/
34+
private static ?WP_Abilities_Registry $instance = null;
35+
36+
/**
37+
* Registers a new ability.
38+
*
39+
* Do not use this method directly. Instead, use the `wp_register_ability()` function.
40+
*
41+
* @see wp_register_ability()
42+
*
43+
* @since 0.1.0
44+
*
45+
* @param string|WP_Ability $name The name of the ability, or WP_Ability instance. The name must be a string
46+
* containing a namespace prefix, i.e. `my-plugin/my-ability`. It can only
47+
* contain lowercase alphanumeric characters, dashes and the forward slash.
48+
* @param array $properties Optional. An associative array of properties for the ability. This should
49+
* include `label`, `description`, `input_schema`, `output_schema`,
50+
* `execute_callback`, `permission_callback`, and `meta`.
51+
* @return ?WP_Ability The registered ability instance on success, null on failure.
52+
*/
53+
public function register( $name, array $properties = array() ): ?WP_Ability {
54+
$ability = null;
55+
if ( $name instanceof WP_Ability ) {
56+
$ability = $name;
57+
$name = $ability->get_name();
58+
}
59+
60+
if ( ! preg_match( '/^[a-z0-9-]+\/[a-z0-9-]+$/', $name ) ) {
61+
_doing_it_wrong(
62+
__METHOD__,
63+
esc_html__(
64+
'Ability name must be a string containing a namespace prefix, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
65+
),
66+
'0.1.0'
67+
);
68+
return null;
69+
}
70+
71+
if ( $this->is_registered( $name ) ) {
72+
_doing_it_wrong(
73+
__METHOD__,
74+
/* translators: %s: Ability name. */
75+
esc_html( sprintf( __( 'Ability "%s" is already registered.' ), $name ) ),
76+
'0.1.0'
77+
);
78+
return null;
79+
}
80+
81+
// If the ability is already an instance, we can skip the rest of the validation.
82+
if ( null !== $ability ) {
83+
$this->registered_abilities[ $name ] = $ability;
84+
return $ability;
85+
}
86+
87+
if ( empty( $properties['label'] ) || ! is_string( $properties['label'] ) ) {
88+
_doing_it_wrong(
89+
__METHOD__,
90+
esc_html__( 'The ability properties must contain a `label` string.' ),
91+
'0.1.0'
92+
);
93+
return null;
94+
}
95+
96+
if ( empty( $properties['description'] ) || ! is_string( $properties['description'] ) ) {
97+
_doing_it_wrong(
98+
__METHOD__,
99+
esc_html__( 'The ability properties must contain a `description` string.' ),
100+
'0.1.0'
101+
);
102+
return null;
103+
}
104+
105+
if ( isset( $properties['input_schema'] ) && ! is_array( $properties['input_schema'] ) ) {
106+
_doing_it_wrong(
107+
__METHOD__,
108+
esc_html__( 'The ability properties should provide a valid `input_schema` definition.' ),
109+
'0.1.0'
110+
);
111+
return null;
112+
}
113+
114+
if ( isset( $properties['output_schema'] ) && ! is_array( $properties['output_schema'] ) ) {
115+
_doing_it_wrong(
116+
__METHOD__,
117+
esc_html__( 'The ability properties should provide a valid `output_schema` definition.' ),
118+
'0.1.0'
119+
);
120+
return null;
121+
}
122+
123+
if ( empty( $properties['execute_callback'] ) || ! is_callable( $properties['execute_callback'] ) ) {
124+
_doing_it_wrong(
125+
__METHOD__,
126+
esc_html__( 'The ability properties must contain a valid `execute_callback` function.' ),
127+
'0.1.0'
128+
);
129+
return null;
130+
}
131+
132+
if ( isset( $properties['permission_callback'] ) && ! is_callable( $properties['permission_callback'] ) ) {
133+
_doing_it_wrong(
134+
__METHOD__,
135+
esc_html__( 'The ability properties should provide a valid `permission_callback` function.' ),
136+
'0.1.0'
137+
);
138+
return null;
139+
}
140+
141+
if ( isset( $properties['meta'] ) && ! is_array( $properties['meta'] ) ) {
142+
_doing_it_wrong(
143+
__METHOD__,
144+
esc_html__( 'The ability properties should provide a valid `meta` array.' ),
145+
'0.1.0'
146+
);
147+
return null;
148+
}
149+
150+
$ability = new WP_Ability(
151+
$name,
152+
array(
153+
'label' => $properties['label'],
154+
'description' => $properties['description'],
155+
'input_schema' => $properties['input_schema'] ?? [],
156+
'output_schema' => $properties['output_schema'] ?? [],
157+
'execute_callback' => $properties['execute_callback'],
158+
'permission_callback' => $properties['permission_callback'] ?? null,
159+
'meta' => $properties['meta'] ?? [],
160+
)
161+
);
162+
$this->registered_abilities[ $name ] = $ability;
163+
return $ability;
164+
}
165+
166+
/**
167+
* Unregisters an ability.
168+
*
169+
* Do not use this method directly. Instead, use the `wp_unregister_ability()` function.
170+
*
171+
* @see wp_unregister_ability()
172+
*
173+
* @since 0.1.0
174+
*
175+
* @param string $name The name of the registered ability, with its namespace.
176+
* @return ?WP_Ability The unregistered ability instance on success, null on failure.
177+
*/
178+
public function unregister( $name ): ?WP_Ability {
179+
if ( ! $this->is_registered( $name ) ) {
180+
_doing_it_wrong(
181+
__METHOD__,
182+
/* translators: %s: Ability name. */
183+
sprintf( esc_html__( 'Ability "%s" not found.' ), esc_attr( $name ) ),
184+
'0.1.0'
185+
);
186+
return null;
187+
}
188+
189+
$unregistered_ability = $this->registered_abilities[ $name ];
190+
unset( $this->registered_abilities[ $name ] );
191+
192+
return $unregistered_ability;
193+
}
194+
195+
/**
196+
* Retrieves the list of all registered abilities.
197+
*
198+
* Do not use this method directly. Instead, use the `wp_get_abilities()` function.
199+
*
200+
* @see wp_get_abilities()
201+
*
202+
* @since 0.1.0
203+
*
204+
* @return WP_Ability[] The array of registered abilities.
205+
*/
206+
public function get_all_registered(): array {
207+
return $this->registered_abilities;
208+
}
209+
210+
/**
211+
* Checks if an ability is registered.
212+
*
213+
* @since 0.1.0
214+
*
215+
* @param string $name The name of the registered ability, with its namespace.
216+
* @return bool True if the ability is registered, false otherwise.
217+
*/
218+
public function is_registered( $name ): bool {
219+
return isset( $this->registered_abilities[ $name ] );
220+
}
221+
222+
/**
223+
* Retrieves a registered ability.
224+
*
225+
* Do not use this method directly. Instead, use the `wp_get_ability()` function.
226+
*
227+
* @see wp_get_ability()
228+
*
229+
* @since 0.1.0
230+
*
231+
* @param string $name The name of the registered ability, with its namespace.
232+
* @return ?WP_Ability The registered ability instance, or null if it is not registered.
233+
*/
234+
public function get_registered( $name ): ?WP_Ability {
235+
if ( ! $this->is_registered( $name ) ) {
236+
_doing_it_wrong(
237+
__METHOD__,
238+
/* translators: %s: Ability name. */
239+
sprintf( esc_html__( 'Ability "%s" not found.' ), esc_attr( $name ) ),
240+
'0.1.0'
241+
);
242+
return null;
243+
}
244+
return $this->registered_abilities[ $name ];
245+
}
246+
247+
/**
248+
* Utility method to retrieve the main instance of the registry class.
249+
*
250+
* The instance will be created if it does not exist yet.
251+
*
252+
* @since 0.1.0
253+
*
254+
* @return WP_Abilities_Registry The main registry instance.
255+
*/
256+
public static function get_instance(): WP_Abilities_Registry {
257+
/* @var WP_Abilities_Registry $wp_abilities */
258+
global $wp_abilities;
259+
260+
if ( empty( $wp_abilities ) ) {
261+
$wp_abilities = new self();
262+
/**
263+
* Fires when preparing abilities registry.
264+
*
265+
* Abilities should be created and register their hooks on this action rather
266+
* than another action to ensure they're only loaded when needed.
267+
*
268+
* @since 0.1.0
269+
*
270+
* @param WP_Abilities_Registry $instance Abilities registry object.
271+
*/
272+
do_action( 'abilities_api_init', $wp_abilities );
273+
}
274+
275+
return $wp_abilities;
276+
}
277+
278+
/**
279+
* Wakeup magic method.
280+
*
281+
* @since 0.1.0
282+
*/
283+
public function __wakeup(): void {
284+
if ( empty( $this->registered_abilities ) ) {
285+
return;
286+
}
287+
288+
foreach ( $this->registered_abilities as $ability ) {
289+
if ( ! $ability instanceof WP_Ability ) {
290+
throw new UnexpectedValueException();
291+
}
292+
}
293+
}
294+
}

0 commit comments

Comments
 (0)