| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jun 13 09:38:55 2026 | Cross-referenced by PHPXref |