Client   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 458
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
dl 0
loc 458
rs 3.04
c 0
b 0
f 0
wmc 67
lcom 1
cbo 14

14 Methods

Rating   Name   Duplication   Size   Complexity  
A send() 0 5 1
B prepareDefaults() 0 34 7
A __construct() 0 15 4
A __call() 0 13 3
A sendAsync() 0 10 1
A sendRequest() 0 8 1
A requestAsync() 0 18 2
A request() 0 5 1
B buildUri() 0 13 7
B configureDefaults() 0 48 10
A transfer() 0 12 2
F applyOptions() 0 122 25
A invalidBody() 0 8 1
A getConfig() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

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']);
0 ignored issues
show
Documentation introduced by
$config['base_uri'] is of type callable, but the function expects a string|object<Psr\Http\Message\UriInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $options = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
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
            $modify['set_headers'] = $options['headers'];
348
            unset($options['headers']);
349
        }
350
351
        if (isset($options['form_params'])) {
352
            if (isset($options['multipart'])) {
353
                throw new InvalidArgumentException('You cannot use '
354
                    . 'form_params and multipart at the same time. Use the '
355
                    . 'form_params option if you want to send application/'
356
                    . 'x-www-form-urlencoded requests, and the multipart '
357
                    . 'option to send multipart/form-data requests.');
358
            }
359
            $options['body'] = \http_build_query($options['form_params'], '', '&');
360
            unset($options['form_params']);
361
            // Ensure that we don't have the header in different case and set the new value.
362
            $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
363
            $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded';
364
        }
365
366
        if (isset($options['multipart'])) {
367
            $options['body'] = new Psr7\MultipartStream($options['multipart']);
368
            unset($options['multipart']);
369
        }
370
371
        if (isset($options['json'])) {
372
            $options['body'] = Utils::jsonEncode($options['json']);
373
            unset($options['json']);
374
            // Ensure that we don't have the header in different case and set the new value.
375
            $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
376
            $options['_conditional']['Content-Type'] = 'application/json';
377
        }
378
379
        if (!empty($options['decode_content'])
380
            && $options['decode_content'] !== true
381
        ) {
382
            // Ensure that we don't have the header in different case and set the new value.
383
            $options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']);
384
            $modify['set_headers']['Accept-Encoding'] = $options['decode_content'];
385
        }
386
387
        if (isset($options['body'])) {
388
            if (\is_array($options['body'])) {
389
                throw $this->invalidBody();
390
            }
391
            $modify['body'] = Psr7\Utils::streamFor($options['body']);
392
            unset($options['body']);
393
        }
394
395
        if (!empty($options['auth']) && \is_array($options['auth'])) {
396
            $value = $options['auth'];
397
            $type = isset($value[2]) ? \strtolower($value[2]) : 'basic';
398
            switch ($type) {
399
                case 'basic':
400
                    // Ensure that we don't have the header in different case and set the new value.
401
                    $modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']);
402
                    $modify['set_headers']['Authorization'] = 'Basic '
403
                        . \base64_encode("$value[0]:$value[1]");
404
                    break;
405
                case 'digest':
406
                    // @todo: Do not rely on curl
407
                    $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST;
408
                    $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
409
                    break;
410
                case 'ntlm':
411
                    $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
412
                    $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
413
                    break;
414
            }
415
        }
416
417
        if (isset($options['query'])) {
418
            $value = $options['query'];
419
            if (\is_array($value)) {
420
                $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986);
421
            }
422
            if (!\is_string($value)) {
423
                throw new InvalidArgumentException('query must be a string or array');
424
            }
425
            $modify['query'] = $value;
426
            unset($options['query']);
427
        }
428
429
        // Ensure that sink is not an invalid value.
430
        if (isset($options['sink'])) {
431
            // TODO: Add more sink validation?
432
            if (\is_bool($options['sink'])) {
433
                throw new InvalidArgumentException('sink must not be a boolean');
434
            }
435
        }
436
437
        $request = Psr7\Utils::modifyRequest($request, $modify);
438
        if ($request->getBody() instanceof Psr7\MultipartStream) {
439
            // Use a multipart/form-data POST if a Content-Type is not set.
440
            // Ensure that we don't have the header in different case and set the new value.
441
            $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
442
            $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary='
443
                . $request->getBody()->getBoundary();
444
        }
445
446
        // Merge in conditional headers if they are not present.
447
        if (isset($options['_conditional'])) {
448
            // Build up the changes so it's in a single clone of the message.
449
            $modify = [];
450
            foreach ($options['_conditional'] as $k => $v) {
451
                if (!$request->hasHeader($k)) {
452
                    $modify['set_headers'][$k] = $v;
453
                }
454
            }
455
            $request = Psr7\Utils::modifyRequest($request, $modify);
456
            // Don't pass this internal value along to middleware/handlers.
457
            unset($options['_conditional']);
458
        }
459
460
        return $request;
461
    }
462
463
    /**
464
     * Return an InvalidArgumentException with pre-set message.
465
     */
466
    private function invalidBody(): InvalidArgumentException
467
    {
468
        return new InvalidArgumentException('Passing in the "body" request '
469
            . 'option as an array to send a request is not supported. '
470
            . 'Please use the "form_params" request option to send a '
471
            . 'application/x-www-form-urlencoded request, or the "multipart" '
472
            . 'request option to send a multipart/form-data request.');
473
    }
474
}
475