run:R W Run
11.52 KB
2026-03-11 16:18:51
R W Run
7.32 KB
2026-03-11 16:18:51
R W Run
5.76 KB
2026-03-11 16:18:51
R W Run
22.23 KB
2026-03-11 16:18:51
R W Run
error_log
📄class-wp-abilities-registry.php
1<?php
2/**
3 * Abilities API
4 *
5 * Defines WP_Abilities_Registry class.
6 *
7 * @package WordPress
8 * @subpackage Abilities API
9 * @since 6.9.0
10 */
11
12declare( strict_types = 1 );
13
14/**
15 * Manages the registration and lookup of abilities.
16 *
17 * @since 6.9.0
18 * @access private
19 */
20final class WP_Abilities_Registry {
21 /**
22 * The singleton instance of the registry.
23 *
24 * @since 6.9.0
25 * @var self|null
26 */
27 private static $instance = null;
28
29 /**
30 * Holds the registered abilities.
31 *
32 * @since 6.9.0
33 * @var WP_Ability[]
34 */
35 private $registered_abilities = array();
36
37 /**
38 * Registers a new ability.
39 *
40 * Do not use this method directly. Instead, use the `wp_register_ability()` function.
41 *
42 * @since 6.9.0
43 *
44 * @see wp_register_ability()
45 *
46 * @param string $name The name of the ability. The name must be a string containing a namespace
47 * prefix, i.e. `my-plugin/my-ability`. It can only contain lowercase
48 * alphanumeric characters, dashes and the forward slash.
49 * @param array<string, mixed> $args {
50 * An associative array of arguments for the ability.
51 *
52 * @type string $label The human-readable label for the ability.
53 * @type string $description A detailed description of what the ability does.
54 * @type string $category The ability category slug this ability belongs to.
55 * @type callable $execute_callback A callback function to execute when the ability is invoked.
56 * Receives optional mixed input and returns mixed result or WP_Error.
57 * @type callable $permission_callback A callback function to check permissions before execution.
58 * Receives optional mixed input and returns bool or WP_Error.
59 * @type array<string, mixed> $input_schema Optional. JSON Schema definition for the ability's input.
60 * @type array<string, mixed> $output_schema Optional. JSON Schema definition for the ability's output.
61 * @type array<string, mixed> $meta {
62 * Optional. Additional metadata for the ability.
63 *
64 * @type array<string, bool|null> $annotations {
65 * Optional. Semantic annotations describing the ability's behavioral characteristics.
66 * These annotations are hints for tooling and documentation.
67 *
68 * @type bool|null $readonly Optional. If true, the ability does not modify its environment.
69 * @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment.
70 * If false, the ability performs only additive updates.
71 * @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments
72 * will have no additional effect on its environment.
73 * }
74 * @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
75 * }
76 * @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability.
77 * }
78 * @return WP_Ability|null The registered ability instance on success, null on failure.
79 */
80 public function register( string $name, array $args ): ?WP_Ability {
81 if ( ! preg_match( '/^[a-z0-9-]+\/[a-z0-9-]+$/', $name ) ) {
82 _doing_it_wrong(
83 __METHOD__,
84 __(
85 '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.'
86 ),
87 '6.9.0'
88 );
89 return null;
90 }
91
92 if ( $this->is_registered( $name ) ) {
93 _doing_it_wrong(
94 __METHOD__,
95 /* translators: %s: Ability name. */
96 sprintf( __( 'Ability "%s" is already registered.' ), esc_html( $name ) ),
97 '6.9.0'
98 );
99 return null;
100 }
101
102 /**
103 * Filters the ability arguments before they are validated and used to instantiate the ability.
104 *
105 * @since 6.9.0
106 *
107 * @param array<string, mixed> $args {
108 * An associative array of arguments for the ability.
109 *
110 * @type string $label The human-readable label for the ability.
111 * @type string $description A detailed description of what the ability does.
112 * @type string $category The ability category slug this ability belongs to.
113 * @type callable $execute_callback A callback function to execute when the ability is invoked.
114 * Receives optional mixed input and returns mixed result or WP_Error.
115 * @type callable $permission_callback A callback function to check permissions before execution.
116 * Receives optional mixed input and returns bool or WP_Error.
117 * @type array<string, mixed> $input_schema Optional. JSON Schema definition for the ability's input.
118 * @type array<string, mixed> $output_schema Optional. JSON Schema definition for the ability's output.
119 * @type array<string, mixed> $meta {
120 * Optional. Additional metadata for the ability.
121 *
122 * @type array<string, bool|string> $annotations Optional. Annotation metadata for the ability.
123 * @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
124 * }
125 * @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability.
126 * }
127 * @param string $name The name of the ability, with its namespace.
128 */
129 $args = apply_filters( 'wp_register_ability_args', $args, $name );
130
131 // Validate ability category exists if provided (will be validated as required in WP_Ability).
132 if ( isset( $args['category'] ) ) {
133 if ( ! wp_has_ability_category( $args['category'] ) ) {
134 _doing_it_wrong(
135 __METHOD__,
136 sprintf(
137 /* translators: %1$s: ability category slug, %2$s: ability name */
138 __( 'Ability category "%1$s" is not registered. Please register the ability category before assigning it to ability "%2$s".' ),
139 esc_html( $args['category'] ),
140 esc_html( $name )
141 ),
142 '6.9.0'
143 );
144 return null;
145 }
146 }
147
148 // The class is only used to instantiate the ability, and is not a property of the ability itself.
149 if ( isset( $args['ability_class'] ) && ! is_a( $args['ability_class'], WP_Ability::class, true ) ) {
150 _doing_it_wrong(
151 __METHOD__,
152 __( 'The ability args should provide a valid `ability_class` that extends WP_Ability.' ),
153 '6.9.0'
154 );
155 return null;
156 }
157
158 /** @var class-string<WP_Ability> */
159 $ability_class = $args['ability_class'] ?? WP_Ability::class;
160 unset( $args['ability_class'] );
161
162 try {
163 // WP_Ability::prepare_properties() will throw an exception if the properties are invalid.
164 $ability = new $ability_class( $name, $args );
165 } catch ( InvalidArgumentException $e ) {
166 _doing_it_wrong(
167 __METHOD__,
168 $e->getMessage(),
169 '6.9.0'
170 );
171 return null;
172 }
173
174 $this->registered_abilities[ $name ] = $ability;
175 return $ability;
176 }
177
178 /**
179 * Unregisters an ability.
180 *
181 * Do not use this method directly. Instead, use the `wp_unregister_ability()` function.
182 *
183 * @since 6.9.0
184 *
185 * @see wp_unregister_ability()
186 *
187 * @param string $name The name of the registered ability, with its namespace.
188 * @return WP_Ability|null The unregistered ability instance on success, null on failure.
189 */
190 public function unregister( string $name ): ?WP_Ability {
191 if ( ! $this->is_registered( $name ) ) {
192 _doing_it_wrong(
193 __METHOD__,
194 /* translators: %s: Ability name. */
195 sprintf( __( 'Ability "%s" not found.' ), esc_html( $name ) ),
196 '6.9.0'
197 );
198 return null;
199 }
200
201 $unregistered_ability = $this->registered_abilities[ $name ];
202 unset( $this->registered_abilities[ $name ] );
203
204 return $unregistered_ability;
205 }
206
207 /**
208 * Retrieves the list of all registered abilities.
209 *
210 * Do not use this method directly. Instead, use the `wp_get_abilities()` function.
211 *
212 * @since 6.9.0
213 *
214 * @see wp_get_abilities()
215 *
216 * @return WP_Ability[] The array of registered abilities.
217 */
218 public function get_all_registered(): array {
219 return $this->registered_abilities;
220 }
221
222 /**
223 * Checks if an ability is registered.
224 *
225 * Do not use this method directly. Instead, use the `wp_has_ability()` function.
226 *
227 * @since 6.9.0
228 *
229 * @see wp_has_ability()
230 *
231 * @param string $name The name of the registered ability, with its namespace.
232 * @return bool True if the ability is registered, false otherwise.
233 */
234 public function is_registered( string $name ): bool {
235 return isset( $this->registered_abilities[ $name ] );
236 }
237
238 /**
239 * Retrieves a registered ability.
240 *
241 * Do not use this method directly. Instead, use the `wp_get_ability()` function.
242 *
243 * @since 6.9.0
244 *
245 * @see wp_get_ability()
246 *
247 * @param string $name The name of the registered ability, with its namespace.
248 * @return WP_Ability|null The registered ability instance, or null if it is not registered.
249 */
250 public function get_registered( string $name ): ?WP_Ability {
251 if ( ! $this->is_registered( $name ) ) {
252 _doing_it_wrong(
253 __METHOD__,
254 /* translators: %s: Ability name. */
255 sprintf( __( 'Ability "%s" not found.' ), esc_html( $name ) ),
256 '6.9.0'
257 );
258 return null;
259 }
260 return $this->registered_abilities[ $name ];
261 }
262
263 /**
264 * Utility method to retrieve the main instance of the registry class.
265 *
266 * The instance will be created if it does not exist yet.
267 *
268 * @since 6.9.0
269 *
270 * @return WP_Abilities_Registry|null The main registry instance, or null when `init` action has not fired.
271 */
272 public static function get_instance(): ?self {
273 if ( ! did_action( 'init' ) ) {
274 _doing_it_wrong(
275 __METHOD__,
276 sprintf(
277 // translators: %s: init action.
278 __( 'Ability API should not be initialized before the %s action has fired.' ),
279 '<code>init</code>'
280 ),
281 '6.9.0'
282 );
283 return null;
284 }
285
286 if ( null === self::$instance ) {
287 self::$instance = new self();
288
289 // Ensure ability category registry is initialized first to allow categories to be registered
290 // before abilities that depend on them.
291 WP_Ability_Categories_Registry::get_instance();
292
293 /**
294 * Fires when preparing abilities registry.
295 *
296 * Abilities should be created and register their hooks on this action rather
297 * than another action to ensure they're only loaded when needed.
298 *
299 * @since 6.9.0
300 *
301 * @param WP_Abilities_Registry $instance Abilities registry object.
302 */
303 do_action( 'wp_abilities_api_init', self::$instance );
304 }
305
306 return self::$instance;
307 }
308
309 /**
310 * Wakeup magic method.
311 *
312 * @since 6.9.0
313 * @throws LogicException If the registry object is unserialized.
314 * This is a security hardening measure to prevent unserialization of the registry.
315 */
316 public function __wakeup(): void {
317 throw new LogicException( __CLASS__ . ' should never be unserialized.' );
318 }
319
320 /**
321 * Sleep magic method.
322 *
323 * @since 6.9.0
324 * @throws LogicException If the registry object is serialized.
325 * This is a security hardening measure to prevent serialization of the registry.
326 */
327 public function __sleep(): array {
328 throw new LogicException( __CLASS__ . ' should never be serialized.' );
329 }
330}
331