| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * WP AI Client: WP_AI_Client_Builder class 4 * 5 * @package WordPress 6 * @subpackage AI 7 * @since 7.2.0 8 */ 9 10 use WordPress\AiClient\Common\Exception\InvalidArgumentException; 11 use WordPress\AiClient\Common\Exception\TokenLimitReachedException; 12 use WordPress\AiClient\Providers\Http\DTO\RequestOptions; 13 use WordPress\AiClient\Providers\Http\Exception\ClientException; 14 use WordPress\AiClient\Providers\Http\Exception\NetworkException; 15 use WordPress\AiClient\Providers\Http\Exception\ServerException; 16 use WordPress\AiClient\Providers\ProviderRegistry; 17 18 /** 19 * Base class for fluent AI client builders, returning WP_Error on failure. 20 * 21 * This class wraps a builder from the PHP AI Client SDK and adds 22 * WordPress-specific behavior shared by all builders: WP_Error handling 23 * instead of exceptions, and snake_case method naming proxied to the SDK's 24 * camelCase methods. 25 * 26 * Only the generating methods will return a WP_Error, to not break the fluent 27 * interface. As soon as any exception is caught in a chain of method calls, 28 * the returned instance will be in an error state, and all subsequent method 29 * calls will be no-ops that just return the same error state instance. Only 30 * when a generating method is called, the WP_Error will be returned. The 31 * support check methods are the exception to the no-op behavior: they return 32 * false rather than the error state instance. 33 * 34 * @since 7.2.0 35 * @internal Intended only as the shared base for the AI client builders in core. 36 * It may gain abstract methods in the future, so it should not be extended. 37 * @access private 38 */ 39 abstract class WP_AI_Client_Builder { 40 41 /** 42 * Wrapped builder instance from the PHP AI Client SDK. 43 * 44 * @since 7.0.0 45 * @since 7.2.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class. 46 * @var object 47 */ 48 private object $builder; 49 50 /** 51 * WordPress error instance, if any error occurred during method calls. 52 * 53 * @since 7.0.0 54 * @since 7.2.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class. 55 * @var WP_Error|null 56 */ 57 private ?WP_Error $error = null; 58 59 /** 60 * Constructor. 61 * 62 * @since 7.0.0 63 * @since 7.2.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class. 64 * 65 * @param ProviderRegistry $registry The provider registry for finding suitable models. 66 * @param mixed $input Optional. Initial input content for the builder. 67 * See the child class for the supported types. 68 * Default null. 69 */ 70 public function __construct( ProviderRegistry $registry, $input = null ) { 71 try { 72 $this->builder = $this->create_sdk_builder( $registry, $input ); 73 } catch ( Throwable $e ) { 74 $this->builder = $this->create_sdk_builder( $registry, null ); 75 $this->error = $this->throwable_to_wp_error( $e ); 76 } 77 78 $default_timeout = 30.0; 79 80 /** 81 * Filters the default request timeout in seconds for AI Client HTTP requests. 82 * 83 * @since 7.0.0 84 * @since 7.2.0 Added the `$builder_type` parameter. 85 * 86 * @param float $default_timeout The default timeout in seconds. 87 * @param string $builder_type The class name of the builder the timeout applies to, 88 * e.g. 'WP_AI_Client_Prompt_Builder' or 'WP_AI_Client_Embedding_Builder'. 89 */ 90 $filtered_default_timeout = apply_filters( 'wp_ai_client_default_request_timeout', $default_timeout, get_class( $this ) ); 91 if ( is_numeric( $filtered_default_timeout ) && (float) $filtered_default_timeout >= 0.0 ) { 92 $default_timeout = (float) $filtered_default_timeout; 93 } else { 94 _doing_it_wrong( 95 get_class( $this ) . '::__construct', 96 sprintf( 97 /* translators: %s: wp_ai_client_default_request_timeout */ 98 __( 'The %s filter must return a non-negative number.' ), 99 '<code>wp_ai_client_default_request_timeout</code>' 100 ), 101 '7.0.0' 102 ); 103 } 104 105 $this->builder->usingRequestOptions( 106 RequestOptions::fromArray( 107 array( 108 RequestOptions::KEY_TIMEOUT => $default_timeout, 109 ) 110 ) 111 ); 112 } 113 114 /** 115 * Clones the wrapped builder alongside this instance. 116 * 117 * The wrapped builder mutates its own state, so without this a clone would 118 * share that state with the original and any change made to one would be 119 * visible in the other. 120 * 121 * @since 7.2.0 122 */ 123 public function __clone() { 124 $this->builder = clone $this->builder; 125 126 if ( null !== $this->error ) { 127 $this->error = clone $this->error; 128 } 129 } 130 131 /** 132 * Creates the wrapped builder instance from the PHP AI Client SDK. 133 * 134 * @since 7.2.0 135 * 136 * @param ProviderRegistry $registry The provider registry for finding suitable models. 137 * @param mixed $input Initial input content for the builder, or null. 138 * @return object The SDK builder instance. 139 */ 140 abstract protected function create_sdk_builder( ProviderRegistry $registry, $input ): object; 141 142 /** 143 * Retrieves the prefix used for WP_Error codes created by this builder. 144 * 145 * @since 7.2.0 146 * 147 * @return string The error code prefix, e.g. 'prompt' or 'embedding'. 148 */ 149 abstract protected function get_error_code_prefix(): string; 150 151 /** 152 * Retrieves the error message to use when execution is prevented by a filter. 153 * 154 * @since 7.2.0 155 * 156 * @return string The translated error message. 157 */ 158 abstract protected function get_prevented_error_message(): string; 159 160 /** 161 * Checks whether execution is prevented by the builder's prevent filter. 162 * 163 * Child classes apply their specific filter, passing a clone of the builder 164 * instance for read-only inspection. 165 * 166 * @since 7.2.0 167 * 168 * @return bool Whether execution is prevented. 169 */ 170 abstract protected function is_prevented_by_filter(): bool; 171 172 /** 173 * Retrieves the methods that generate a result from the builder. 174 * 175 * Structured as a map of method name to true for faster lookups. 176 * 177 * @since 7.2.0 178 * 179 * @return array<string, bool> The generating methods map. 180 */ 181 abstract protected function get_generating_methods(): array; 182 183 /** 184 * Retrieves the methods that check whether the builder input is supported. 185 * 186 * Structured as a map of method name to true for faster lookups. 187 * 188 * @since 7.2.0 189 * 190 * @return array<string, bool> The support check methods map. 191 */ 192 abstract protected function get_support_check_methods(): array; 193 194 /** 195 * Magic method to proxy snake_case method calls to their PHP AI Client camelCase counterparts. 196 * 197 * This allows WordPress developers to use snake_case naming conventions. It catches 198 * any exceptions thrown, stores them, and returns a WP_Error when a terminate method 199 * is called, or false when a support check method is called. 200 * 201 * @since 7.0.0 202 * @since 7.2.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class. 203 * 204 * @param string $name The method name in snake_case. 205 * @param array<int, mixed> $arguments The method arguments. 206 * @return mixed The result of the method call. 207 */ 208 public function __call( string $name, array $arguments ) { 209 /* 210 * If an error occurred in a previous method call, either return the error for terminate methods, 211 * or return the same instance for other methods to maintain the fluent interface. 212 */ 213 if ( null !== $this->error ) { 214 if ( $this->is_generating_method( $name ) ) { 215 return $this->error; 216 } 217 if ( $this->is_support_check_method( $name ) ) { 218 return false; 219 } 220 return $this; 221 } 222 223 // Check if execution should be prevented for support check and generating methods. 224 if ( $this->is_support_check_method( $name ) || $this->is_generating_method( $name ) ) { 225 // If AI is not supported, then there's no need to apply the filter as execution will be prevented anyway. 226 $is_ai_disabled = ! wp_supports_ai(); 227 $prevent = $is_ai_disabled; 228 if ( ! $prevent ) { 229 $prevent = $this->is_prevented_by_filter(); 230 } 231 232 if ( $prevent ) { 233 // For support check methods, return false. 234 if ( $this->is_support_check_method( $name ) ) { 235 return false; 236 } 237 238 $error_message = $is_ai_disabled 239 ? __( 'AI features are not supported in this environment.' ) 240 : $this->get_prevented_error_message(); 241 242 // For generating methods, create a WP_Error. 243 $this->error = new WP_Error( 244 $this->get_error_code_prefix() . '_prevented', 245 $error_message, 246 array( 247 'status' => 503, 248 ) 249 ); 250 251 if ( $this->is_generating_method( $name ) ) { 252 return $this->error; 253 } 254 return $this; 255 } 256 } 257 258 try { 259 $callable = $this->get_builder_callable( $name ); 260 $result = $callable( ...$arguments ); 261 262 // If the result is the wrapped SDK builder, return the current instance to allow method chaining. 263 if ( $result instanceof $this->builder ) { 264 return $this; 265 } 266 267 return $result; 268 } catch ( Throwable $e ) { 269 $this->error = $this->throwable_to_wp_error( $e ); 270 271 if ( $this->is_generating_method( $name ) ) { 272 return $this->error; 273 } 274 if ( $this->is_support_check_method( $name ) ) { 275 return false; 276 } 277 return $this; 278 } 279 } 280 281 /** 282 * Converts a throwable into a WP_Error with a structured error code and message. 283 * 284 * This method maps different throwable types to specific WP_Error codes and HTTP status codes. 285 * The presence of the status codes means these WP_Error objects can be easily used in REST API responses 286 * or other contexts where HTTP semantics are relevant. 287 * 288 * @since 7.0.0 289 * @since 7.2.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class. 290 * 291 * @param Throwable $throwable The throwable to convert. 292 * @return WP_Error The resulting WP_Error object. 293 */ 294 private function throwable_to_wp_error( Throwable $throwable ): WP_Error { 295 $prefix = $this->get_error_code_prefix(); 296 297 if ( $throwable instanceof NetworkException ) { 298 $error_code = $prefix . '_network_error'; 299 $status_code = 503; 300 } elseif ( $throwable instanceof ClientException ) { 301 // `ClientException` uses HTTP status codes as exception codes, so we can rely on them. 302 $error_code = $prefix . '_client_error'; 303 $status_code = $throwable->getCode() ? $throwable->getCode() : 400; 304 } elseif ( $throwable instanceof ServerException ) { 305 // `ServerException` uses HTTP status codes as exception codes, so we can rely on them. 306 $error_code = $prefix . '_upstream_server_error'; 307 $status_code = $throwable->getCode() ? $throwable->getCode() : 500; 308 } elseif ( $throwable instanceof TokenLimitReachedException ) { 309 $error_code = $prefix . '_token_limit_reached'; 310 $status_code = 400; 311 } elseif ( $throwable instanceof InvalidArgumentException ) { 312 $error_code = $prefix . '_invalid_argument'; 313 $status_code = 400; 314 } else { 315 $error_code = $prefix . '_builder_error'; 316 $status_code = 500; 317 } 318 319 return new WP_Error( 320 $error_code, 321 $throwable->getMessage(), 322 array( 323 'status' => $status_code, 324 'exception_class' => get_class( $throwable ), 325 ) 326 ); 327 } 328 329 /** 330 * Checks if a method name is a support check method (is_supported*). 331 * 332 * @since 7.0.0 333 * @since 7.2.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class, 334 * and changed from static to instance method. 335 * 336 * @param string $name The method name. 337 * @return bool True if the method is a support check method, false otherwise. 338 */ 339 private function is_support_check_method( string $name ): bool { 340 $methods = $this->get_support_check_methods(); 341 return isset( $methods[ $name ] ); 342 } 343 344 /** 345 * Checks if a method name is a generating method (generate_*, convert_text_to_speech*). 346 * 347 * @since 7.0.0 348 * @since 7.2.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class, 349 * and changed from static to instance method. 350 * 351 * @param string $name The method name. 352 * @return bool True if the method is a generating method, false otherwise. 353 */ 354 private function is_generating_method( string $name ): bool { 355 $methods = $this->get_generating_methods(); 356 return isset( $methods[ $name ] ); 357 } 358 359 /** 360 * Retrieves a callable for a given PHP AI Client SDK builder method name. 361 * 362 * @since 7.0.0 363 * @since 7.2.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class. 364 * 365 * @param string $name The method name in snake_case. 366 * @return callable The callable for the specified method. 367 * 368 * @throws BadMethodCallException If the method does not exist. 369 */ 370 protected function get_builder_callable( string $name ): callable { 371 $camel_case_name = $this->snake_to_camel_case( $name ); 372 373 $method = array( $this->builder, $camel_case_name ); 374 if ( ! is_callable( $method ) ) { 375 throw new BadMethodCallException( 376 sprintf( 377 /* translators: 1: Method name. 2: Class name. */ 378 __( 'Method %1$s does not exist on %2$s.' ), 379 $name, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 380 get_class( $this->builder ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 381 ) 382 ); 383 } 384 385 return $method; 386 } 387 388 /** 389 * Converts snake_case to camelCase. 390 * 391 * @since 7.0.0 392 * @since 7.2.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class. 393 * 394 * @param string $snake_case The snake_case string. 395 * @return string The camelCase string. 396 */ 397 private function snake_to_camel_case( string $snake_case ): string { 398 $parts = explode( '_', $snake_case ); 399 400 $camel_case = $parts[0]; 401 $parts_count = count( $parts ); 402 for ( $i = 1; $i < $parts_count; $i++ ) { 403 $camel_case .= ucfirst( $parts[ $i ] ); 404 } 405 406 return $camel_case; 407 } 408 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Oct 14 08:20:33 2026 | Cross-referenced by PHPXref |