1 <?php 2 3 namespace GuzzleHttp; 4 5 use GuzzleHttp\Cookie\CookieJar; 6 use GuzzleHttp\Exception\GuzzleException; 7 use GuzzleHttp\Exception\InvalidArgumentException; 8 use GuzzleHttp\Promise as P; 9 use GuzzleHttp\Promise\PromiseInterface; 10 use Psr\Http\Message\RequestInterface; 11 use Psr\Http\Message\ResponseInterface; 12 use Psr\Http\Message\UriInterface; 13 14 /** 15 * @final 16 */ 17 class Client implements ClientInterface, \Psr\Http\Client\ClientInterface 18 { 19 use ClientTrait; 20 21 /** 22 * @var array Default request options 23 */ 24 private $config; 25 26 /** 27 * Clients accept an array of constructor parameters. 28 * 29 * Here's an example of creating a client using a base_uri and an array of 30 * default request options to apply to each request: 31 * 32 * $client = new Client([ 33 * 'base_uri' => 'http://www.foo.com/1.0/', 34 * 'timeout' => 0, 35 * 'allow_redirects' => false, 36 * 'proxy' => '192.168.16.1:10' 37 * ]); 38 * 39 * Client configuration settings include the following options: 40 * 41 * - handler: (callable) Function that transfers HTTP requests over the 42 * wire. The function is called with a Psr7\Http\Message\RequestInterface 43 * and array of transfer options, and must return a 44 * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a 45 * Psr7\Http\Message\ResponseInterface on success. 46 * If no handler is provided, a default handler will be created 47 * that enables all of the request options below by attaching all of the 48 * default middleware to the handler. 49 * - base_uri: (string|UriInterface) Base URI of the client that is merged 50 * into relative URIs. Can be a string or instance of UriInterface. 51 * - **: any request option 52 * 53 * @param array $config Client configuration settings. 54 * 55 * @see \GuzzleHttp\RequestOptions for a list of available request options. 56 */ 57 public function __construct(array $config = []) 58 { 59 if (!isset($config['handler'])) { 60 $config['handler'] = HandlerStack::create(); 61 } elseif (!\is_callable($config['handler'])) { 62 throw new InvalidArgumentException('handler must be a callable'); 63 } 64 65 // Convert the base_uri to a UriInterface 66 if (isset($config['base_uri'])) { 67 $config['base_uri'] = Psr7\Utils::uriFor($config['base_uri']); 68 } 69 70 $this->configureDefaults($config); 71 } 72 73 /** 74 * @param string $method 75 * @param array $args 76 * 77 * @return PromiseInterface|ResponseInterface 78 * 79 * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0. 80 */ 81 public function __call($method, $args) 82 { 83 if (\count($args) < 1) { 84 throw new InvalidArgumentException('Magic request methods require a URI and optional options array'); 85 } 86 87 $uri = $args[0]; 88 $opts = $args[1] ?? []; 89 90 return \substr($method, -5) === 'Async' 91 ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts) 92 : $this->request($method, $uri, $opts); 93 } 94 95 /** 96 * Asynchronously send an HTTP request. 97 * 98 * @param array $options Request options to apply to the given 99 * request and to the transfer. See \GuzzleHttp\RequestOptions. 100 */ 101 public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface 102 { 103 // Merge the base URI into the request URI if needed. 104 $options = $this->prepareDefaults($options); 105 106 return $this->transfer( 107 $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), 108 $options 109 ); 110 } 111 112 /** 113 * Send an HTTP request. 114 * 115 * @param array $options Request options to apply to the given 116 * request and to the transfer. See \GuzzleHttp\RequestOptions. 117 * 118 * @throws GuzzleException 119 */ 120 public function send(RequestInterface $request, array $options = []): ResponseInterface 121 { 122 $options[RequestOptions::SYNCHRONOUS] = true; 123 return $this->sendAsync($request, $options)->wait(); 124 } 125 126 /** 127 * The HttpClient PSR (PSR-18) specify this method. 128 * 129 * @inheritDoc 130 */ 131 public function sendRequest(RequestInterface $request): ResponseInterface 132 { 133 $options[RequestOptions::SYNCHRONOUS] = true; 134 $options[RequestOptions::ALLOW_REDIRECTS] = false; 135 $options[RequestOptions::HTTP_ERRORS] = false; 136 137 return $this->sendAsync($request, $options)->wait(); 138 } 139 140 /** 141 * Create and send an asynchronous HTTP request. 142 * 143 * Use an absolute path to override the base path of the client, or a 144 * relative path to append to the base path of the client. The URL can 145 * contain the query string as well. Use an array to provide a URL 146 * template and additional variables to use in the URL template expansion. 147 * 148 * @param string $method HTTP method 149 * @param string|UriInterface $uri URI object or string. 150 * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. 151 */ 152 public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface 153 { 154 $options = $this->prepareDefaults($options); 155 // Remove request modifying parameter because it can be done up-front. 156 $headers = $options['headers'] ?? []; 157 $body = $options['body'] ?? null; 158 $version = $options['version'] ?? '1.1'; 159 // Merge the URI into the base URI. 160 $uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options); 161 if (\is_array($body)) { 162 throw $this->invalidBody(); 163 } 164 $request = new Psr7\Request($method, $uri, $headers, $body, $version); 165 // Remove the option so that they are not doubly-applied. 166 unset($options['headers'], $options['body'], $options['version']); 167 168 return $this->transfer($request, $options); 169 } 170 171 /** 172 * Create and send an HTTP request. 173 * 174 * Use an absolute path to override the base path of the client, or a 175 * relative path to append to the base path of the client. The URL can 176 * contain the query string as well. 177 * 178 * @param string $method HTTP method. 179 * @param string|UriInterface $uri URI object or string. 180 * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. 181 * 182 * @throws GuzzleException 183 */ 184 public function request(string $method, $uri = '', array $options = []): ResponseInterface 185 { 186 $options[RequestOptions::SYNCHRONOUS] = true; 187 return $this->requestAsync($method, $uri, $options)->wait(); 188 } 189 190 /** 191 * Get a client configuration option. 192 * 193 * These options include default request options of the client, a "handler" 194 * (if utilized by the concrete client), and a "base_uri" if utilized by 195 * the concrete client. 196 * 197 * @param string|null $option The config option to retrieve. 198 * 199 * @return mixed 200 * 201 * @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0. 202 */ 203 public function getConfig(?string $option = null) 204 { 205 return $option === null 206 ? $this->config 207 : ($this->config[$option] ?? null); 208 } 209 210 private function buildUri(UriInterface $uri, array $config): UriInterface 211 { 212 if (isset($config['base_uri'])) { 213 $uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri); 214 } 215 216 if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) { 217 $idnOptions = ($config['idn_conversion'] === true) ? \IDNA_DEFAULT : $config['idn_conversion']; 218 $uri = Utils::idnUriConvert($uri, $idnOptions); 219 } 220 221 return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; 222 } 223 224 /** 225 * Configures the default options for a client. 226 */ 227 private function configureDefaults(array $config): void 228 { 229 $defaults = [ 230 'allow_redirects' => RedirectMiddleware::$defaultSettings, 231 'http_errors' => true, 232 'decode_content' => true, 233 'verify' => true, 234 'cookies' => false, 235 'idn_conversion' => false, 236 ]; 237 238 // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. 239 240 // We can only trust the HTTP_PROXY environment variable in a CLI 241 // process due to the fact that PHP has no reliable mechanism to 242 // get environment variables that start with "HTTP_". 243 if (\PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) { 244 $defaults['proxy']['http'] = $proxy; 245 } 246 247 if ($proxy = Utils::getenv('HTTPS_PROXY')) { 248 $defaults['proxy']['https'] = $proxy; 249 } 250 251 if ($noProxy = Utils::getenv('NO_PROXY')) { 252 $cleanedNoProxy = \str_replace(' ', '', $noProxy); 253 $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy); 254 } 255 256 $this->config = $config + $defaults; 257 258 if (!empty($config['cookies']) && $config['cookies'] === true) { 259 $this->config['cookies'] = new CookieJar(); 260 } 261 262 // Add the default user-agent header. 263 if (!isset($this->config['headers'])) { 264 $this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()]; 265 } else { 266 // Add the User-Agent header if one was not already set. 267 foreach (\array_keys($this->config['headers']) as $name) { 268 if (\strtolower($name) === 'user-agent') { 269 return; 270 } 271 } 272 $this->config['headers']['User-Agent'] = Utils::defaultUserAgent(); 273 } 274 } 275 276 /** 277 * Merges default options into the array. 278 * 279 * @param array $options Options to modify by reference 280 */ 281 private function prepareDefaults(array $options): array 282 { 283 $defaults = $this->config; 284 285 if (!empty($defaults['headers'])) { 286 // Default headers are only added if they are not present. 287 $defaults['_conditional'] = $defaults['headers']; 288 unset($defaults['headers']); 289 } 290 291 // Special handling for headers is required as they are added as 292 // conditional headers and as headers passed to a request ctor. 293 if (\array_key_exists('headers', $options)) { 294 // Allows default headers to be unset. 295 if ($options['headers'] === null) { 296 $defaults['_conditional'] = []; 297 unset($options['headers']); 298 } elseif (!\is_array($options['headers'])) { 299 throw new InvalidArgumentException('headers must be an array'); 300 } 301 } 302 303 // Shallow merge defaults underneath options. 304 $result = $options + $defaults; 305 306 // Remove null values. 307 foreach ($result as $k => $v) { 308 if ($v === null) { 309 unset($result[$k]); 310 } 311 } 312 313 return $result; 314 } 315 316 /** 317 * Transfers the given request and applies request options. 318 * 319 * The URI of the request is not modified and the request options are used 320 * as-is without merging in default options. 321 * 322 * @param array $options See \GuzzleHttp\RequestOptions. 323 */ 324 private function transfer(RequestInterface $request, array $options): PromiseInterface 325 { 326 $request = $this->applyOptions($request, $options); 327 /** @var HandlerStack $handler */ 328 $handler = $options['handler']; 329 330 try { 331 return P\Create::promiseFor($handler($request, $options)); 332 } catch (\Exception $e) { 333 return P\Create::rejectionFor($e); 334 } 335 } 336 337 /** 338 * Applies the array of request options to a request. 339 */ 340 private function applyOptions(RequestInterface $request, array &$options): RequestInterface 341 { 342 $modify = [ 343 'set_headers' => [], 344 ]; 345 346 if (isset($options['headers'])) { 347 if (array_keys($options['headers']) === range(0, count($options['headers']) - 1)) { 348 throw new InvalidArgumentException('The headers array must have header name as keys.'); 349 } 350 $modify['set_headers'] = $options['headers']; 351 unset($options['headers']); 352 } 353 354 if (isset($options['form_params'])) { 355 if (isset($options['multipart'])) { 356 throw new InvalidArgumentException('You cannot use ' 357 . 'form_params and multipart at the same time. Use the ' 358 . 'form_params option if you want to send application/' 359 . 'x-www-form-urlencoded requests, and the multipart ' 360 . 'option to send multipart/form-data requests.'); 361 } 362 $options['body'] = \http_build_query($options['form_params'], '', '&'); 363 unset($options['form_params']); 364 // Ensure that we don't have the header in different case and set the new value. 365 $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); 366 $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; 367 } 368 369 if (isset($options['multipart'])) { 370 $options['body'] = new Psr7\MultipartStream($options['multipart']); 371 unset($options['multipart']); 372 } 373 374 if (isset($options['json'])) { 375 $options['body'] = Utils::jsonEncode($options['json']); 376 unset($options['json']); 377 // Ensure that we don't have the header in different case and set the new value. 378 $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); 379 $options['_conditional']['Content-Type'] = 'application/json'; 380 } 381 382 if (!empty($options['decode_content']) 383 && $options['decode_content'] !== true 384 ) { 385 // Ensure that we don't have the header in different case and set the new value. 386 $options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']); 387 $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; 388 } 389 390 if (isset($options['body'])) { 391 if (\is_array($options['body'])) { 392 throw $this->invalidBody(); 393 } 394 $modify['body'] = Psr7\Utils::streamFor($options['body']); 395 unset($options['body']); 396 } 397 398 if (!empty($options['auth']) && \is_array($options['auth'])) { 399 $value = $options['auth']; 400 $type = isset($value[2]) ? \strtolower($value[2]) : 'basic'; 401 switch ($type) { 402 case 'basic': 403 // Ensure that we don't have the header in different case and set the new value. 404 $modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']); 405 $modify['set_headers']['Authorization'] = 'Basic ' 406 . \base64_encode("$value[0]:$value[1]"); 407 break; 408 case 'digest': 409 // @todo: Do not rely on curl 410 $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST; 411 $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; 412 break; 413 case 'ntlm': 414 $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM; 415 $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; 416 break; 417 } 418 } 419 420 if (isset($options['query'])) { 421 $value = $options['query']; 422 if (\is_array($value)) { 423 $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986); 424 } 425 if (!\is_string($value)) { 426 throw new InvalidArgumentException('query must be a string or array'); 427 } 428 $modify['query'] = $value; 429 unset($options['query']); 430 } 431 432 // Ensure that sink is not an invalid value. 433 if (isset($options['sink'])) { 434 // TODO: Add more sink validation? 435 if (\is_bool($options['sink'])) { 436 throw new InvalidArgumentException('sink must not be a boolean'); 437 } 438 } 439 440 $request = Psr7\Utils::modifyRequest($request, $modify); 441 if ($request->getBody() instanceof Psr7\MultipartStream) { 442 // Use a multipart/form-data POST if a Content-Type is not set. 443 // Ensure that we don't have the header in different case and set the new value. 444 $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); 445 $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' 446 . $request->getBody()->getBoundary(); 447 } 448 449 // Merge in conditional headers if they are not present. 450 if (isset($options['_conditional'])) { 451 // Build up the changes so it's in a single clone of the message. 452 $modify = []; 453 foreach ($options['_conditional'] as $k => $v) { 454 if (!$request->hasHeader($k)) { 455 $modify['set_headers'][$k] = $v; 456 } 457 } 458 $request = Psr7\Utils::modifyRequest($request, $modify); 459 // Don't pass this internal value along to middleware/handlers. 460 unset($options['_conditional']); 461 } 462 463 return $request; 464 } 465 466 /** 467 * Return an InvalidArgumentException with pre-set message. 468 */ 469 private function invalidBody(): InvalidArgumentException 470 { 471 return new InvalidArgumentException('Passing in the "body" request ' 472 . 'option as an array to send a request is not supported. ' 473 . 'Please use the "form_params" request option to send a ' 474 . 'application/x-www-form-urlencoded request, or the "multipart" ' 475 . 'request option to send a multipart/form-data request.'); 476 } 477 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body