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 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 |
|
---|
124 | return $this->sendAsync($request, $options)->wait();
|
---|
125 | }
|
---|
126 |
|
---|
127 | /**
|
---|
128 | * The HttpClient PSR (PSR-18) specify this method.
|
---|
129 | *
|
---|
130 | * {@inheritDoc}
|
---|
131 | */
|
---|
132 | public function sendRequest(RequestInterface $request): ResponseInterface
|
---|
133 | {
|
---|
134 | $options[RequestOptions::SYNCHRONOUS] = true;
|
---|
135 | $options[RequestOptions::ALLOW_REDIRECTS] = false;
|
---|
136 | $options[RequestOptions::HTTP_ERRORS] = false;
|
---|
137 |
|
---|
138 | return $this->sendAsync($request, $options)->wait();
|
---|
139 | }
|
---|
140 |
|
---|
141 | /**
|
---|
142 | * Create and send an asynchronous HTTP request.
|
---|
143 | *
|
---|
144 | * Use an absolute path to override the base path of the client, or a
|
---|
145 | * relative path to append to the base path of the client. The URL can
|
---|
146 | * contain the query string as well. Use an array to provide a URL
|
---|
147 | * template and additional variables to use in the URL template expansion.
|
---|
148 | *
|
---|
149 | * @param string $method HTTP method
|
---|
150 | * @param string|UriInterface $uri URI object or string.
|
---|
151 | * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
|
---|
152 | */
|
---|
153 | public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
|
---|
154 | {
|
---|
155 | $options = $this->prepareDefaults($options);
|
---|
156 | // Remove request modifying parameter because it can be done up-front.
|
---|
157 | $headers = $options['headers'] ?? [];
|
---|
158 | $body = $options['body'] ?? null;
|
---|
159 | $version = $options['version'] ?? '1.1';
|
---|
160 | // Merge the URI into the base URI.
|
---|
161 | $uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options);
|
---|
162 | if (\is_array($body)) {
|
---|
163 | throw $this->invalidBody();
|
---|
164 | }
|
---|
165 | $request = new Psr7\Request($method, $uri, $headers, $body, $version);
|
---|
166 | // Remove the option so that they are not doubly-applied.
|
---|
167 | unset($options['headers'], $options['body'], $options['version']);
|
---|
168 |
|
---|
169 | return $this->transfer($request, $options);
|
---|
170 | }
|
---|
171 |
|
---|
172 | /**
|
---|
173 | * Create and send an HTTP request.
|
---|
174 | *
|
---|
175 | * Use an absolute path to override the base path of the client, or a
|
---|
176 | * relative path to append to the base path of the client. The URL can
|
---|
177 | * contain the query string as well.
|
---|
178 | *
|
---|
179 | * @param string $method HTTP method.
|
---|
180 | * @param string|UriInterface $uri URI object or string.
|
---|
181 | * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
|
---|
182 | *
|
---|
183 | * @throws GuzzleException
|
---|
184 | */
|
---|
185 | public function request(string $method, $uri = '', array $options = []): ResponseInterface
|
---|
186 | {
|
---|
187 | $options[RequestOptions::SYNCHRONOUS] = true;
|
---|
188 |
|
---|
189 | return $this->requestAsync($method, $uri, $options)->wait();
|
---|
190 | }
|
---|
191 |
|
---|
192 | /**
|
---|
193 | * Get a client configuration option.
|
---|
194 | *
|
---|
195 | * These options include default request options of the client, a "handler"
|
---|
196 | * (if utilized by the concrete client), and a "base_uri" if utilized by
|
---|
197 | * the concrete client.
|
---|
198 | *
|
---|
199 | * @param string|null $option The config option to retrieve.
|
---|
200 | *
|
---|
201 | * @return mixed
|
---|
202 | *
|
---|
203 | * @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0.
|
---|
204 | */
|
---|
205 | public function getConfig(?string $option = null)
|
---|
206 | {
|
---|
207 | return $option === null
|
---|
208 | ? $this->config
|
---|
209 | : ($this->config[$option] ?? null);
|
---|
210 | }
|
---|
211 |
|
---|
212 | private function buildUri(UriInterface $uri, array $config): UriInterface
|
---|
213 | {
|
---|
214 | if (isset($config['base_uri'])) {
|
---|
215 | $uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri);
|
---|
216 | }
|
---|
217 |
|
---|
218 | if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) {
|
---|
219 | $idnOptions = ($config['idn_conversion'] === true) ? \IDNA_DEFAULT : $config['idn_conversion'];
|
---|
220 | $uri = Utils::idnUriConvert($uri, $idnOptions);
|
---|
221 | }
|
---|
222 |
|
---|
223 | return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri;
|
---|
224 | }
|
---|
225 |
|
---|
226 | /**
|
---|
227 | * Configures the default options for a client.
|
---|
228 | */
|
---|
229 | private function configureDefaults(array $config): void
|
---|
230 | {
|
---|
231 | $defaults = [
|
---|
232 | 'allow_redirects' => RedirectMiddleware::$defaultSettings,
|
---|
233 | 'http_errors' => true,
|
---|
234 | 'decode_content' => true,
|
---|
235 | 'verify' => true,
|
---|
236 | 'cookies' => false,
|
---|
237 | 'idn_conversion' => false,
|
---|
238 | ];
|
---|
239 |
|
---|
240 | // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set.
|
---|
241 |
|
---|
242 | // We can only trust the HTTP_PROXY environment variable in a CLI
|
---|
243 | // process due to the fact that PHP has no reliable mechanism to
|
---|
244 | // get environment variables that start with "HTTP_".
|
---|
245 | if (\PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) {
|
---|
246 | $defaults['proxy']['http'] = $proxy;
|
---|
247 | }
|
---|
248 |
|
---|
249 | if ($proxy = Utils::getenv('HTTPS_PROXY')) {
|
---|
250 | $defaults['proxy']['https'] = $proxy;
|
---|
251 | }
|
---|
252 |
|
---|
253 | if ($noProxy = Utils::getenv('NO_PROXY')) {
|
---|
254 | $cleanedNoProxy = \str_replace(' ', '', $noProxy);
|
---|
255 | $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy);
|
---|
256 | }
|
---|
257 |
|
---|
258 | $this->config = $config + $defaults;
|
---|
259 |
|
---|
260 | if (!empty($config['cookies']) && $config['cookies'] === true) {
|
---|
261 | $this->config['cookies'] = new CookieJar();
|
---|
262 | }
|
---|
263 |
|
---|
264 | // Add the default user-agent header.
|
---|
265 | if (!isset($this->config['headers'])) {
|
---|
266 | $this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()];
|
---|
267 | } else {
|
---|
268 | // Add the User-Agent header if one was not already set.
|
---|
269 | foreach (\array_keys($this->config['headers']) as $name) {
|
---|
270 | if (\strtolower($name) === 'user-agent') {
|
---|
271 | return;
|
---|
272 | }
|
---|
273 | }
|
---|
274 | $this->config['headers']['User-Agent'] = Utils::defaultUserAgent();
|
---|
275 | }
|
---|
276 | }
|
---|
277 |
|
---|
278 | /**
|
---|
279 | * Merges default options into the array.
|
---|
280 | *
|
---|
281 | * @param array $options Options to modify by reference
|
---|
282 | */
|
---|
283 | private function prepareDefaults(array $options): array
|
---|
284 | {
|
---|
285 | $defaults = $this->config;
|
---|
286 |
|
---|
287 | if (!empty($defaults['headers'])) {
|
---|
288 | // Default headers are only added if they are not present.
|
---|
289 | $defaults['_conditional'] = $defaults['headers'];
|
---|
290 | unset($defaults['headers']);
|
---|
291 | }
|
---|
292 |
|
---|
293 | // Special handling for headers is required as they are added as
|
---|
294 | // conditional headers and as headers passed to a request ctor.
|
---|
295 | if (\array_key_exists('headers', $options)) {
|
---|
296 | // Allows default headers to be unset.
|
---|
297 | if ($options['headers'] === null) {
|
---|
298 | $defaults['_conditional'] = [];
|
---|
299 | unset($options['headers']);
|
---|
300 | } elseif (!\is_array($options['headers'])) {
|
---|
301 | throw new InvalidArgumentException('headers must be an array');
|
---|
302 | }
|
---|
303 | }
|
---|
304 |
|
---|
305 | // Shallow merge defaults underneath options.
|
---|
306 | $result = $options + $defaults;
|
---|
307 |
|
---|
308 | // Remove null values.
|
---|
309 | foreach ($result as $k => $v) {
|
---|
310 | if ($v === null) {
|
---|
311 | unset($result[$k]);
|
---|
312 | }
|
---|
313 | }
|
---|
314 |
|
---|
315 | return $result;
|
---|
316 | }
|
---|
317 |
|
---|
318 | /**
|
---|
319 | * Transfers the given request and applies request options.
|
---|
320 | *
|
---|
321 | * The URI of the request is not modified and the request options are used
|
---|
322 | * as-is without merging in default options.
|
---|
323 | *
|
---|
324 | * @param array $options See \GuzzleHttp\RequestOptions.
|
---|
325 | */
|
---|
326 | private function transfer(RequestInterface $request, array $options): PromiseInterface
|
---|
327 | {
|
---|
328 | $request = $this->applyOptions($request, $options);
|
---|
329 | /** @var HandlerStack $handler */
|
---|
330 | $handler = $options['handler'];
|
---|
331 |
|
---|
332 | try {
|
---|
333 | return P\Create::promiseFor($handler($request, $options));
|
---|
334 | } catch (\Exception $e) {
|
---|
335 | return P\Create::rejectionFor($e);
|
---|
336 | }
|
---|
337 | }
|
---|
338 |
|
---|
339 | /**
|
---|
340 | * Applies the array of request options to a request.
|
---|
341 | */
|
---|
342 | private function applyOptions(RequestInterface $request, array &$options): RequestInterface
|
---|
343 | {
|
---|
344 | $modify = [
|
---|
345 | 'set_headers' => [],
|
---|
346 | ];
|
---|
347 |
|
---|
348 | if (isset($options['headers'])) {
|
---|
349 | if (array_keys($options['headers']) === range(0, count($options['headers']) - 1)) {
|
---|
350 | throw new InvalidArgumentException('The headers array must have header name as keys.');
|
---|
351 | }
|
---|
352 | $modify['set_headers'] = $options['headers'];
|
---|
353 | unset($options['headers']);
|
---|
354 | }
|
---|
355 |
|
---|
356 | if (isset($options['form_params'])) {
|
---|
357 | if (isset($options['multipart'])) {
|
---|
358 | throw new InvalidArgumentException('You cannot use '
|
---|
359 | .'form_params and multipart at the same time. Use the '
|
---|
360 | .'form_params option if you want to send application/'
|
---|
361 | .'x-www-form-urlencoded requests, and the multipart '
|
---|
362 | .'option to send multipart/form-data requests.');
|
---|
363 | }
|
---|
364 | $options['body'] = \http_build_query($options['form_params'], '', '&');
|
---|
365 | unset($options['form_params']);
|
---|
366 | // Ensure that we don't have the header in different case and set the new value.
|
---|
367 | $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
|
---|
368 | $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded';
|
---|
369 | }
|
---|
370 |
|
---|
371 | if (isset($options['multipart'])) {
|
---|
372 | $options['body'] = new Psr7\MultipartStream($options['multipart']);
|
---|
373 | unset($options['multipart']);
|
---|
374 | }
|
---|
375 |
|
---|
376 | if (isset($options['json'])) {
|
---|
377 | $options['body'] = Utils::jsonEncode($options['json']);
|
---|
378 | unset($options['json']);
|
---|
379 | // Ensure that we don't have the header in different case and set the new value.
|
---|
380 | $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
|
---|
381 | $options['_conditional']['Content-Type'] = 'application/json';
|
---|
382 | }
|
---|
383 |
|
---|
384 | if (!empty($options['decode_content'])
|
---|
385 | && $options['decode_content'] !== true
|
---|
386 | ) {
|
---|
387 | // Ensure that we don't have the header in different case and set the new value.
|
---|
388 | $options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']);
|
---|
389 | $modify['set_headers']['Accept-Encoding'] = $options['decode_content'];
|
---|
390 | }
|
---|
391 |
|
---|
392 | if (isset($options['body'])) {
|
---|
393 | if (\is_array($options['body'])) {
|
---|
394 | throw $this->invalidBody();
|
---|
395 | }
|
---|
396 | $modify['body'] = Psr7\Utils::streamFor($options['body']);
|
---|
397 | unset($options['body']);
|
---|
398 | }
|
---|
399 |
|
---|
400 | if (!empty($options['auth']) && \is_array($options['auth'])) {
|
---|
401 | $value = $options['auth'];
|
---|
402 | $type = isset($value[2]) ? \strtolower($value[2]) : 'basic';
|
---|
403 | switch ($type) {
|
---|
404 | case 'basic':
|
---|
405 | // Ensure that we don't have the header in different case and set the new value.
|
---|
406 | $modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']);
|
---|
407 | $modify['set_headers']['Authorization'] = 'Basic '
|
---|
408 | .\base64_encode("$value[0]:$value[1]");
|
---|
409 | break;
|
---|
410 | case 'digest':
|
---|
411 | // @todo: Do not rely on curl
|
---|
412 | $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST;
|
---|
413 | $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
|
---|
414 | break;
|
---|
415 | case 'ntlm':
|
---|
416 | $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
|
---|
417 | $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
|
---|
418 | break;
|
---|
419 | }
|
---|
420 | }
|
---|
421 |
|
---|
422 | if (isset($options['query'])) {
|
---|
423 | $value = $options['query'];
|
---|
424 | if (\is_array($value)) {
|
---|
425 | $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986);
|
---|
426 | }
|
---|
427 | if (!\is_string($value)) {
|
---|
428 | throw new InvalidArgumentException('query must be a string or array');
|
---|
429 | }
|
---|
430 | $modify['query'] = $value;
|
---|
431 | unset($options['query']);
|
---|
432 | }
|
---|
433 |
|
---|
434 | // Ensure that sink is not an invalid value.
|
---|
435 | if (isset($options['sink'])) {
|
---|
436 | // TODO: Add more sink validation?
|
---|
437 | if (\is_bool($options['sink'])) {
|
---|
438 | throw new InvalidArgumentException('sink must not be a boolean');
|
---|
439 | }
|
---|
440 | }
|
---|
441 |
|
---|
442 | if (isset($options['version'])) {
|
---|
443 | $modify['version'] = $options['version'];
|
---|
444 | }
|
---|
445 |
|
---|
446 | $request = Psr7\Utils::modifyRequest($request, $modify);
|
---|
447 | if ($request->getBody() instanceof Psr7\MultipartStream) {
|
---|
448 | // Use a multipart/form-data POST if a Content-Type is not set.
|
---|
449 | // Ensure that we don't have the header in different case and set the new value.
|
---|
450 | $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
|
---|
451 | $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary='
|
---|
452 | .$request->getBody()->getBoundary();
|
---|
453 | }
|
---|
454 |
|
---|
455 | // Merge in conditional headers if they are not present.
|
---|
456 | if (isset($options['_conditional'])) {
|
---|
457 | // Build up the changes so it's in a single clone of the message.
|
---|
458 | $modify = [];
|
---|
459 | foreach ($options['_conditional'] as $k => $v) {
|
---|
460 | if (!$request->hasHeader($k)) {
|
---|
461 | $modify['set_headers'][$k] = $v;
|
---|
462 | }
|
---|
463 | }
|
---|
464 | $request = Psr7\Utils::modifyRequest($request, $modify);
|
---|
465 | // Don't pass this internal value along to middleware/handlers.
|
---|
466 | unset($options['_conditional']);
|
---|
467 | }
|
---|
468 |
|
---|
469 | return $request;
|
---|
470 | }
|
---|
471 |
|
---|
472 | /**
|
---|
473 | * Return an InvalidArgumentException with pre-set message.
|
---|
474 | */
|
---|
475 | private function invalidBody(): InvalidArgumentException
|
---|
476 | {
|
---|
477 | return new InvalidArgumentException('Passing in the "body" request '
|
---|
478 | .'option as an array to send a request is not supported. '
|
---|
479 | .'Please use the "form_params" request option to send a '
|
---|
480 | .'application/x-www-form-urlencoded request, or the "multipart" '
|
---|
481 | .'request option to send a multipart/form-data request.');
|
---|
482 | }
|
---|
483 | }
|
---|