[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ai-client/ -> class-wp-ai-client-builder.php (source)

   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  }


Generated : Wed Oct 14 08:20:33 2026 Cross-referenced by PHPXref