PluginProbe
AI / trunk
AI vtrunk
1.3.0 1.2.0 1.1.0 1.0.2 1.0.1 1.0.0 0.9.0 trunk 0.1.1 0.2.0 0.2.1 0.3.0 0.3.1 0.4.0 0.4.1 0.5.0 0.6.0 0.7.0 0.8.0
ai / includes / Experiments / Abilities_Explorer / Ability_Handler.php

Ability_Handler.php in AI trunk, at includes/Experiments/Abilities_Explorer/Ability_Handler.php

447 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability Handler Class
4 *
5 * Handles fetching and processing abilities from
6 * the WordPress Abilities API.
7 *
8 * @package WordPress\AI\Experiments\Abilities_Explorer
9 * @since 0.2.0
10 */
11
12 declare( strict_types=1 );
13
14 namespace WordPress\AI\Experiments\Abilities_Explorer;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * Ability Handler Class
22 *
23 * Provides methods for retrieving, formatting,
24 * and invoking WordPress abilities.
25 *
26 * @since 0.2.0
27 */
28 class Ability_Handler {
29
30 /**
31 * Get all registered abilities.
32 *
33 * @since 0.2.0
34 *
35 * @return array<array<string,mixed>> Array of abilities.
36 */
37 public static function get_all_abilities(): array {
38 return self::format_abilities( wp_get_abilities() );
39 }
40
41 /**
42 * Get a single ability by slug.
43 *
44 * @since 0.2.0
45 *
46 * @param string $slug Ability slug (name).
47 * @return array<string,mixed>|null Ability data or null if not found.
48 */
49 public static function get_ability( string $slug ): ?array {
50 $ability = wp_get_ability( $slug );
51
52 if ( ! $ability ) {
53 return null;
54 }
55
56 return self::format_single_ability( $ability );
57 }
58
59 /**
60 * Format abilities array.
61 *
62 * @since 0.2.0
63 *
64 * @param array<\WP_Ability> $abilities Raw abilities array (WP_Ability objects).
65 * @return array<array<string,mixed>> Formatted abilities.
66 */
67 private static function format_abilities( array $abilities ): array {
68 if ( empty( $abilities ) ) {
69 return array();
70 }
71
72 $formatted = array();
73
74 foreach ( $abilities as $ability ) {
75 $formatted[] = self::format_single_ability( $ability );
76 }
77
78 return $formatted;
79 }
80
81 /**
82 * Format a single ability.
83 *
84 * @since 0.2.0
85 *
86 * @param \WP_Ability $ability Ability object.
87 * @return array<string,mixed> Formatted ability data.
88 */
89 private static function format_single_ability( \WP_Ability $ability ): array {
90 $name = $ability->get_name();
91 $meta = $ability->get_meta();
92
93 return array(
94 'slug' => $name,
95 'name' => $ability->get_label(),
96 'description' => $ability->get_description(),
97 'provider' => self::detect_provider( $name, $meta ),
98 'origin' => self::detect_origin( $name ),
99 'category' => self::get_ability_category( $ability ),
100 'input_schema' => $ability->get_input_schema(),
101 'output_schema' => $ability->get_output_schema(),
102 'raw_data' => array(
103 'name' => $name,
104 'label' => $ability->get_label(),
105 'description' => $ability->get_description(),
106 'input_schema' => $ability->get_input_schema(),
107 'output_schema' => $ability->get_output_schema(),
108 'meta' => $meta,
109 ),
110 );
111 }
112
113 /**
114 * Get the category for an ability.
115 *
116 * @since 0.7.0
117 *
118 * @param \WP_Ability $ability Ability object.
119 * @return string Category for the ability.
120 */
121 public static function get_ability_category( \WP_Ability $ability ): string {
122 $slug = $ability->get_name();
123 $category_slug = $ability->get_category();
124
125 $category_label = esc_html__( 'Other', 'ai' );
126 if ( ! empty( $category_slug ) ) {
127 $category_obj = wp_get_ability_category( $category_slug );
128 if ( $category_obj ) {
129 $category_label = $category_obj->get_label();
130 }
131 }
132
133 /**
134 * Filters the final resolved category for a specific ability.
135 *
136 * Use this hook to explicitly override the category for a
137 * specific ability slug.
138 *
139 * Example:
140 * add_filter( 'wpai_ability_category', function( $category, $slug ) {
141 * if ( 'my-plugin/generate-meta-description' === $slug ) {
142 * return 'SEO';
143 * }
144 * return $category;
145 * }, 10, 2 );
146 *
147 * @since 0.7.0
148 *
149 * @param string $category Resolved category for this ability.
150 * @param string $slug The full ability slug, e.g. 'my-plugin/do-thing'.
151 */
152 return (string) apply_filters( 'wpai_ability_category', $category_label, $slug );
153 }
154
155 /**
156 * Get translatable provider labels keyed by provider slug.
157 *
158 * @since 0.4.0
159 *
160 * @return array<string,string> Map of provider slug to translated label.
161 */
162 public static function get_provider_labels(): array {
163 return array(
164 'Core' => __( 'Core', 'ai' ),
165 'Plugin' => __( 'Plugin', 'ai' ),
166 'Theme' => __( 'Theme', 'ai' ),
167 );
168 }
169
170 /**
171 * Get the label for a provider.
172 *
173 * @since 0.4.0
174 *
175 * @param string $provider Provider slug.
176 * @return string Provider label.
177 */
178 public static function get_provider_label( string $provider ): string {
179 return self::get_provider_labels()[ $provider ] ?? $provider;
180 }
181
182 /**
183 * Detect ability provider (Core, Plugin, or Theme).
184 *
185 * @since 0.2.0
186 *
187 * @param string $name Ability name (slug).
188 * @param array<string,mixed> $meta Ability metadata.
189 * @return string Provider type.
190 */
191 private static function detect_provider( string $name, array $meta ): string {
192 // Check if provider is explicitly set in meta.
193 if ( isset( $meta['provider'] ) ) {
194 return $meta['provider'];
195 }
196
197 return self::detect_origin( $name );
198 }
199
200 /**
201 * Detects the origin (Core, Plugin, or Theme) of an ability from its name.
202 *
203 * Unlike the provider, which can be overridden with a custom label via the
204 * ability's `meta['provider']`, the origin always resolves to one of the
205 * three known buckets, so it is suitable for aggregate statistics.
206 *
207 * @since 1.3.0
208 *
209 * @param string $name Ability name (slug).
210 * @return string Origin type: 'Core', 'Plugin', or 'Theme'.
211 */
212 private static function detect_origin( string $name ): string {
213 // Detect based on name prefix (namespace/ability format).
214 $parts = explode( '/', $name );
215 if ( count( $parts ) === 2 ) {
216 $namespace = $parts[0];
217
218 // WordPress core abilities.
219 if ( in_array( $namespace, array( 'wordpress', 'wp', 'core' ), true ) ) {
220 return 'Core';
221 }
222
223 // Check if namespace matches active theme.
224 if ( get_stylesheet() === $namespace || get_template() === $namespace ) {
225 return 'Theme';
226 }
227 }
228
229 // Default to Plugin.
230 return 'Plugin';
231 }
232
233 /**
234 * Invoke an ability.
235 *
236 * @since 0.2.0
237 *
238 * @param string $slug Ability name.
239 * @param mixed $input Input data. May be an array for object schemas or a
240 * scalar for non-object input schemas.
241 * @return array Result with success status and data/error.
242 *
243 * @phpstan-return array{
244 * success: bool,
245 * code?: int|string,
246 * data?: mixed,
247 * error?: string,
248 * }
249 */
250 public static function invoke_ability( string $slug, $input = null ): array {
251 $ability = wp_get_ability( $slug );
252
253 if ( ! $ability ) {
254 return array(
255 'success' => false,
256 'error' => sprintf( 'Ability "%s" not found', $slug ),
257 );
258 }
259
260 // If ability has no input schema, invoke without input.
261 $input_schema = $ability->get_input_schema();
262 if ( empty( $input_schema ) ) {
263 $result = $ability->execute();
264 } else {
265 $result = $ability->execute( $input );
266 }
267
268 // Check if result is WP_Error.
269 if ( is_wp_error( $result ) ) {
270 return array(
271 'success' => false,
272 'error' => $result->get_error_message(),
273 'code' => $result->get_error_code(),
274 'data' => $result->get_error_data(),
275 );
276 }
277
278 return array(
279 'success' => true,
280 'data' => $result,
281 );
282 }
283
284 /**
285 * Validate input against input schema.
286 *
287 * @since 0.2.0
288 *
289 * @param array<string,mixed> $schema Input schema.
290 * @param mixed $input Input data to validate.
291 * @return array<string,bool|array<string>> Validation result.
292 */
293 public static function validate_input( array $schema, $input ): array {
294 $errors = array();
295
296 if ( empty( $schema ) ) {
297 return array(
298 'valid' => true,
299 'errors' => array(),
300 );
301 }
302
303 // Basic JSON Schema validation.
304 if ( isset( $schema['required'] ) && is_array( $schema['required'] ) ) {
305 foreach ( $schema['required'] as $required_field ) {
306 if ( isset( $input[ $required_field ] ) ) {
307 continue;
308 }
309
310 $errors[] = sprintf( 'Required field "%s" is missing', $required_field );
311 }
312 }
313
314 // Type and constraint validation for properties.
315 if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
316 foreach ( $schema['properties'] as $prop_name => $prop_schema ) {
317 if ( ! isset( $input[ $prop_name ] ) || ! is_array( $prop_schema ) ) {
318 continue;
319 }
320
321 $errors = array_merge(
322 $errors,
323 self::validate_property( (string) $prop_name, $input[ $prop_name ], $prop_schema )
324 );
325 }
326 }
327
328 return array(
329 'valid' => empty( $errors ),
330 'errors' => $errors,
331 );
332 }
333
334 /**
335 * Validate a property value against a schema.
336 *
337 * @since 1.0.2
338 *
339 * @param string $prop_name Property name.
340 * @param mixed $value Property value.
341 * @param array<string,mixed> $prop_schema Property schema.
342 * @return array<string> Validation errors.
343 */
344 private static function validate_property( string $prop_name, $value, array $prop_schema ): array {
345 $errors = array();
346
347 if ( isset( $prop_schema['type'] ) ) {
348 $valid = self::validate_type( $value, $prop_schema['type'] );
349 if ( ! $valid ) {
350 return array(
351 sprintf(
352 'Field "%s" should be of type "%s"',
353 $prop_name,
354 $prop_schema['type']
355 ),
356 );
357 }
358 }
359
360 if ( isset( $prop_schema['enum'] ) && is_array( $prop_schema['enum'] ) && ! in_array( $value, $prop_schema['enum'], true ) ) {
361 $errors[] = sprintf(
362 'Field "%s" must be one of: %s',
363 $prop_name,
364 implode( ', ', $prop_schema['enum'] )
365 );
366 }
367
368 if ( is_numeric( $value ) && isset( $prop_schema['minimum'] ) && $value < $prop_schema['minimum'] ) {
369 $errors[] = sprintf(
370 'Field "%s" must be at least %s',
371 $prop_name,
372 $prop_schema['minimum']
373 );
374 }
375
376 if ( is_numeric( $value ) && isset( $prop_schema['maximum'] ) && $value > $prop_schema['maximum'] ) {
377 $errors[] = sprintf(
378 'Field "%s" must be at most %s',
379 $prop_name,
380 $prop_schema['maximum']
381 );
382 }
383
384 return $errors;
385 }
386
387 /**
388 * Validate value type.
389 *
390 * @since 0.2.0
391 *
392 * @param mixed $value Value to validate.
393 * @param string $expected_type Expected type.
394 * @return bool Whether the value matches the expected type.
395 */
396 private static function validate_type( $value, string $expected_type ): bool {
397 switch ( $expected_type ) {
398 case 'string':
399 return is_string( $value );
400 case 'number':
401 return is_int( $value ) || is_float( $value );
402 case 'integer':
403 return is_int( $value ) || ( is_float( $value ) && floor( $value ) === $value );
404 case 'boolean':
405 return is_bool( $value );
406 case 'array':
407 return is_array( $value );
408 case 'object':
409 return is_object( $value ) || is_array( $value );
410 default:
411 return true;
412 }
413 }
414
415 /**
416 * Get ability statistics.
417 *
418 * @since 0.2.0
419 *
420 * @return array<string,int|array<string,int>> Statistics about registered abilities.
421 */
422 public static function get_statistics(): array {
423 $abilities = self::get_all_abilities();
424
425 $stats = array(
426 'total' => count( $abilities ),
427 'by_provider' => array(
428 'Core' => 0,
429 'Plugin' => 0,
430 'Theme' => 0,
431 ),
432 );
433
434 foreach ( $abilities as $ability ) {
435 // Count by origin so abilities with a custom provider label still
436 // land in their Core/Plugin/Theme bucket.
437 if ( ! isset( $ability['origin'], $stats['by_provider'][ $ability['origin'] ] ) ) {
438 continue;
439 }
440
441 ++$stats['by_provider'][ $ability['origin'] ];
442 }
443
444 return $stats;
445 }
446 }
447