[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/php-ai-client/src/Providers/Http/ -> HttpTransporter.php (source)

   1  <?php
   2  
   3  declare (strict_types=1);
   4  namespace WordPress\AiClient\Providers\Http;
   5  
   6  use WordPress\AiClientDependencies\Http\Discovery\Psr17FactoryDiscovery;
   7  use WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery;
   8  use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;
   9  use WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface;
  10  use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
  11  use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
  12  use WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface;
  13  use WordPress\AiClient\Common\Exception\RuntimeException;
  14  use WordPress\AiClient\Providers\Http\Contracts\ClientWithOptionsInterface;
  15  use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
  16  use WordPress\AiClient\Providers\Http\DTO\Request;
  17  use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
  18  use WordPress\AiClient\Providers\Http\DTO\Response;
  19  use WordPress\AiClient\Providers\Http\Exception\NetworkException;
  20  /**
  21   * HTTP transporter implementation using HTTPlug.
  22   *
  23   * This class handles the conversion between custom Request/Response
  24   * objects and PSR-7 messages, using HTTPlug for client abstraction
  25   * and PSR-17 factories for message creation.
  26   *
  27   * @since 0.1.0
  28   */
  29  class HttpTransporter implements HttpTransporterInterface
  30  {
  31      /**
  32       * @var RequestFactoryInterface PSR-17 request factory.
  33       */
  34      private RequestFactoryInterface $requestFactory;
  35      /**
  36       * @var StreamFactoryInterface PSR-17 stream factory.
  37       */
  38      private StreamFactoryInterface $streamFactory;
  39      /**
  40       * @var ClientInterface PSR-18 HTTP client.
  41       */
  42      private ClientInterface $client;
  43      /**
  44       * Constructor.
  45       *
  46       * @since 0.1.0
  47       *
  48       * @param ClientInterface|null $client PSR-18 HTTP client.
  49       * @param RequestFactoryInterface|null $requestFactory PSR-17 request factory.
  50       * @param StreamFactoryInterface|null $streamFactory PSR-17 stream factory.
  51       */
  52      public function __construct(?ClientInterface $client = null, ?RequestFactoryInterface $requestFactory = null, ?StreamFactoryInterface $streamFactory = null)
  53      {
  54          $this->client = $client ?: Psr18ClientDiscovery::find();
  55          $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory();
  56          $this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory();
  57      }
  58      /**
  59       * {@inheritDoc}
  60       *
  61       * @since 0.1.0
  62       * @since 0.2.0 Added optional RequestOptions parameter and ClientWithOptions support.
  63       */
  64      public function send(Request $request, ?RequestOptions $options = null): Response
  65      {
  66          $psr7Request = $this->convertToPsr7Request($request);
  67          // Merge request options with parameter options, with parameter options taking precedence
  68          $mergedOptions = $this->mergeOptions($request->getOptions(), $options);
  69          try {
  70              $hasOptions = $mergedOptions !== null;
  71              if ($hasOptions && $this->client instanceof ClientWithOptionsInterface) {
  72                  $psr7Response = $this->client->sendRequestWithOptions($psr7Request, $mergedOptions);
  73              } elseif ($hasOptions && $this->isGuzzleClient($this->client)) {
  74                  $psr7Response = $this->sendWithGuzzle($psr7Request, $mergedOptions);
  75              } else {
  76                  $psr7Response = $this->client->sendRequest($psr7Request);
  77              }
  78          } catch (\WordPress\AiClientDependencies\Psr\Http\Client\NetworkExceptionInterface $e) {
  79              throw NetworkException::fromPsr18NetworkException($psr7Request, $e);
  80          } catch (\WordPress\AiClientDependencies\Psr\Http\Client\ClientExceptionInterface $e) {
  81              // Handle other PSR-18 client exceptions that are not network-related
  82              throw new RuntimeException(sprintf('HTTP client error occurred while sending request to %s: %s', $request->getUri(), $e->getMessage()), 0, $e);
  83          }
  84          return $this->convertFromPsr7Response($psr7Response);
  85      }
  86      /**
  87       * Merges request options with parameter options taking precedence.
  88       *
  89       * @since 0.2.0
  90       *
  91       * @param RequestOptions|null $requestOptions Options from the Request object.
  92       * @param RequestOptions|null $parameterOptions Options passed as method parameter.
  93       * @return RequestOptions|null Merged options, or null if both are null.
  94       */
  95      private function mergeOptions(?RequestOptions $requestOptions, ?RequestOptions $parameterOptions): ?RequestOptions
  96      {
  97          // If no options at all, return null
  98          if ($requestOptions === null && $parameterOptions === null) {
  99              return null;
 100          }
 101          // If only one set of options exists, return it
 102          if ($requestOptions === null) {
 103              return $parameterOptions;
 104          }
 105          if ($parameterOptions === null) {
 106              return $requestOptions;
 107          }
 108          // Both exist, merge them with parameter options taking precedence
 109          $merged = new RequestOptions();
 110          // Start with request options (lower precedence)
 111          if ($requestOptions->getTimeout() !== null) {
 112              $merged->setTimeout($requestOptions->getTimeout());
 113          }
 114          if ($requestOptions->getConnectTimeout() !== null) {
 115              $merged->setConnectTimeout($requestOptions->getConnectTimeout());
 116          }
 117          if ($requestOptions->getMaxRedirects() !== null) {
 118              $merged->setMaxRedirects($requestOptions->getMaxRedirects());
 119          }
 120          // Override with parameter options (higher precedence)
 121          if ($parameterOptions->getTimeout() !== null) {
 122              $merged->setTimeout($parameterOptions->getTimeout());
 123          }
 124          if ($parameterOptions->getConnectTimeout() !== null) {
 125              $merged->setConnectTimeout($parameterOptions->getConnectTimeout());
 126          }
 127          if ($parameterOptions->getMaxRedirects() !== null) {
 128              $merged->setMaxRedirects($parameterOptions->getMaxRedirects());
 129          }
 130          return $merged;
 131      }
 132      /**
 133       * Determines if the underlying client matches the Guzzle client shape.
 134       *
 135       * @since 0.2.0
 136       *
 137       * @param ClientInterface $client The HTTP client instance.
 138       * @return bool True when the client exposes Guzzle's send signature.
 139       */
 140      private function isGuzzleClient(ClientInterface $client): bool
 141      {
 142          $reflection = new \ReflectionObject($client);
 143          if (!is_callable([$client, 'send'])) {
 144              return \false;
 145          }
 146          if (!$reflection->hasMethod('send')) {
 147              return \false;
 148          }
 149          $method = $reflection->getMethod('send');
 150          if (!$method->isPublic() || $method->isStatic()) {
 151              return \false;
 152          }
 153          $parameters = $method->getParameters();
 154          if (count($parameters) < 2) {
 155              return \false;
 156          }
 157          $firstParameter = $parameters[0]->getType();
 158          if (!$firstParameter instanceof \ReflectionNamedType || $firstParameter->isBuiltin()) {
 159              return \false;
 160          }
 161          if (!is_a($firstParameter->getName(), RequestInterface::class, \true)) {
 162              return \false;
 163          }
 164          $secondParameter = $parameters[1];
 165          $secondType = $secondParameter->getType();
 166          if (!$secondType instanceof \ReflectionNamedType || $secondType->getName() !== 'array') {
 167              return \false;
 168          }
 169          return \true;
 170      }
 171      /**
 172       * Sends a request using a Guzzle-compatible client.
 173       *
 174       * @since 0.2.0
 175       *
 176       * @param RequestInterface $request The PSR-7 request to send.
 177       * @param RequestOptions $options The request options.
 178       * @return ResponseInterface The PSR-7 response received.
 179       */
 180      private function sendWithGuzzle(RequestInterface $request, RequestOptions $options): ResponseInterface
 181      {
 182          $guzzleOptions = $this->buildGuzzleOptions($options);
 183          /** @var callable $callable */
 184          $callable = [$this->client, 'send'];
 185          /** @var ResponseInterface $response */
 186          $response = $callable($request, $guzzleOptions);
 187          return $response;
 188      }
 189      /**
 190       * Converts request options to a Guzzle-compatible options array.
 191       *
 192       * @since 0.2.0
 193       *
 194       * @param RequestOptions $options The request options.
 195       * @return array<string, mixed> Guzzle-compatible options.
 196       */
 197      private function buildGuzzleOptions(RequestOptions $options): array
 198      {
 199          $guzzleOptions = [];
 200          $timeout = $options->getTimeout();
 201          if ($timeout !== null) {
 202              $guzzleOptions['timeout'] = $timeout;
 203          }
 204          $connectTimeout = $options->getConnectTimeout();
 205          if ($connectTimeout !== null) {
 206              $guzzleOptions['connect_timeout'] = $connectTimeout;
 207          }
 208          $allowRedirects = $options->allowsRedirects();
 209          if ($allowRedirects !== null) {
 210              if ($allowRedirects) {
 211                  $redirectOptions = [];
 212                  $maxRedirects = $options->getMaxRedirects();
 213                  if ($maxRedirects !== null) {
 214                      $redirectOptions['max'] = $maxRedirects;
 215                  }
 216                  $guzzleOptions['allow_redirects'] = !empty($redirectOptions) ? $redirectOptions : \true;
 217              } else {
 218                  $guzzleOptions['allow_redirects'] = \false;
 219              }
 220          }
 221          return $guzzleOptions;
 222      }
 223      /**
 224       * Converts a custom Request to a PSR-7 request.
 225       *
 226       * @since 0.1.0
 227       *
 228       * @param Request $request The custom request.
 229       * @return RequestInterface The PSR-7 request.
 230       */
 231      private function convertToPsr7Request(Request $request): RequestInterface
 232      {
 233          $psr7Request = $this->requestFactory->createRequest($request->getMethod()->value, $request->getUri());
 234          // Add headers
 235          foreach ($request->getHeaders() as $name => $values) {
 236              foreach ($values as $value) {
 237                  $psr7Request = $psr7Request->withAddedHeader($name, $value);
 238              }
 239          }
 240          // Add body if present
 241          $body = $request->getBody();
 242          if ($body !== null) {
 243              $stream = $this->streamFactory->createStream($body);
 244              $psr7Request = $psr7Request->withBody($stream);
 245          }
 246          return $psr7Request;
 247      }
 248      /**
 249       * Converts a PSR-7 response to a custom Response.
 250       *
 251       * @since 0.1.0
 252       *
 253       * @param ResponseInterface $psr7Response The PSR-7 response.
 254       * @return Response The custom response.
 255       */
 256      private function convertFromPsr7Response(ResponseInterface $psr7Response): Response
 257      {
 258          $body = (string) $psr7Response->getBody();
 259          // PSR-7 always returns headers as arrays, but HeadersCollection handles this
 260          return new Response(
 261              $psr7Response->getStatusCode(),
 262              $psr7Response->getHeaders(),
 263              // @phpstan-ignore-line
 264              $body === '' ? null : $body
 265          );
 266      }
 267  }


Generated : Sat Jun 13 09:38:55 2026 Cross-referenced by PHPXref