Completed
Push — master ( ea68f4...dd10a9 )
by Elf
02:27
created

HttpClient   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 513
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 3

Test Coverage

Coverage 99.25%

Importance

Changes 0
Metric Value
wmc 53
lcom 2
cbo 3
dl 0
loc 513
ccs 133
cts 134
cp 0.9925
rs 7.4757
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A defaultOptions() 0 4 1
A setDefaultOptions() 0 4 1
A __construct() 0 14 4
A getClient() 0 4 1
A areExceptionsCaught() 0 4 1
A catchExceptions() 0 6 1
A getOption() 0 4 1
A option() 0 10 3
A mergeOptions() 0 6 1
A removeOption() 0 6 2
A header() 0 4 1
A accept() 0 4 1
A acceptJson() 0 4 1
A userAgent() 0 4 1
A contentType() 0 4 1
A saveTo() 0 4 1
A getResponse() 0 4 1
A getResponseData() 0 10 3
A getContent() 0 4 1
A getJsonContent() 0 4 1
A request() 0 17 3
A requestJson() 0 8 1
A addAcceptableJsonType() 0 11 2
A fetchContent() 0 4 1
A fetchJson() 0 4 1
A getMagicRequestMethods() 0 6 1
A isMagicRequestMethod() 0 18 4
A getRequestParameters() 0 10 2
A getMagicResponseMethods() 0 7 1
A getMagicOptionMethods() 0 14 2
A getOptionKeyForMethod() 0 6 2
B __call() 0 22 5

How to fix   Complexity   

Complex Class

Complex classes like HttpClient 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 HttpClient, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace ElfSundae;
4
5
use Closure;
6
use Exception;
7
use ReflectionClass;
8
use GuzzleHttp\Client;
9
use BadMethodCallException;
10
use Illuminate\Support\Arr;
11
use Illuminate\Support\Str;
12
use InvalidArgumentException;
13
use GuzzleHttp\RequestOptions;
14
use Psr\Http\Message\UriInterface;
15
16
/**
17
 * @method $this get(string|UriInterface $uri = '', array $options = [])
18
 * @method $this head(string|UriInterface $uri = '', array $options = [])
19
 * @method $this post(string|UriInterface $uri = '', array $options = [])
20
 * @method $this put(string|UriInterface $uri = '', array $options = [])
21
 * @method $this patch(string|UriInterface $uri = '', array $options = [])
22
 * @method $this delete(string|UriInterface $uri = '', array $options = [])
23
 * @method $this options(string|UriInterface $uri = '', array $options = [])
24
 * @method $this getJson(string|UriInterface $uri = '', array $options = [])
25
 * @method $this postJson(string|UriInterface $uri = '', array $options = [])
26
 * @method $this putJson(string|UriInterface $uri = '', array $options = [])
27
 * @method $this patchJson(string|UriInterface $uri = '', array $options = [])
28
 * @method $this deleteJson(string|UriInterface $uri = '', array $options = [])
29
 * @method int getStatusCode()
30
 * @method string getReasonPhrase()
31
 * @method string getProtocolVersion()
32
 * @method array getHeaders()
33
 * @method bool hasHeader(string $header)
34
 * @method array getHeader(string $header)
35
 * @method string getHeaderLine(string $header)
36
 * @method \Psr\Http\Message\StreamInterface getBody()
37
 * @method $this allowRedirects(bool|array $value)
38
 * @method $this auth(array|string|null $value)
39
 * @method $this body(mixed $value)
40
 * @method $this cert(string|array $value)
41
 * @method $this cookies(bool|\GuzzleHttp\Cookie\CookieJarInterface $value)
42
 * @method $this connectTimeout(float $value)
43
 * @method $this debug(bool|resource $value)
44
 * @method $this decodeContent(bool $value)
45
 * @method $this delay(int|float $value)
46
 * @method $this expect(bool|int $value)
47
 * @method $this formParams(array $value)
48
 * @method $this headers(array $value)
49
 * @method $this httpErrors(bool $value)
50
 * @method $this json(mixed $value)
51
 * @method $this multipart(array $value)
52
 * @method $this onHeaders(callable $value)
53
 * @method $this onStats(callable $value)
54
 * @method $this progress(callable $value)
55
 * @method $this proxy(string|array $value)
56
 * @method $this query(array|string $value)
57
 * @method $this sink(string|resource|\Psr\Http\Message\StreamInterface $value)
58
 * @method $this sslKey(array|string $value)
59
 * @method $this stream(bool $value)
60
 * @method $this verify(bool|string $value)
61
 * @method $this timeout(float $value)
62
 * @method $this readTimeout(float $value)
63
 * @method $this version(float|string $value)
64
 * @method $this forceIpResolve(string $value)
65
 *
66
 * @see http://docs.guzzlephp.org/en/stable/request-options.html Request Options
67
 */
68
class HttpClient
69
{
70
    /**
71
     * The default request options.
72
     *
73
     * @var array
74
     */
75
    protected static $defaultOptions = [
76
        'connect_timeout' => 5,
77
        'timeout' => 20,
78
        'http_errors' => false,
79
    ];
80
81
    /**
82
     * The Guzzle client.
83
     *
84
     * @var \GuzzleHttp\Client
85
     */
86
    protected $client;
87
88
    /**
89
     * The request options.
90
     *
91
     * @var array
92
     */
93
    protected $options = [];
94
95
    /**
96
     * The Guzzle response.
97
     *
98
     * @var \GuzzleHttp\Psr7\Response
99
     */
100
    protected $response;
101
102
    /**
103
     * Indicate whether to catch Guzzle exceptions.
104
     *
105
     * @var bool
106
     */
107
    protected $catchExceptions = true;
108
109
    /**
110
     * Get the default request options.
111
     *
112
     * @return array
113
     */
114 27
    public static function defaultOptions()
115
    {
116 27
        return static::$defaultOptions;
117
    }
118
119
    /**
120
     * Set the default request options.
121
     *
122
     * @param  array  $options
123
     * @return void
124
     */
125 28
    public static function setDefaultOptions(array $options)
126
    {
127 28
        static::$defaultOptions = $options;
128 28
    }
129
130
    /**
131
     * Create a http client instance.
132
     *
133
     * @param  array|string|\Psr\Http\Message\UriInterface  $options  base URI or any request options
134
     *
135
     * @throws \InvalidArgumentException
136
     */
137 27
    public function __construct($options = [])
138
    {
139 27
        if (is_string($options) || $options instanceof UriInterface) {
140 1
            $options = ['base_uri' => $options];
141 27
        } elseif (! is_array($options)) {
142 1
            throw new InvalidArgumentException('Options must be a string, UriInterface, or an array');
143
        }
144
145 26
        $this->client = new Client(
146 26
            array_replace_recursive(static::defaultOptions(), $options)
147 26
        );
148
149 26
        $this->options = $this->client->getConfig();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->client->getConfig() of type * is incompatible with the declared type array of property $options.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
150 26
    }
151
152
    /**
153
     * Get the Guzzle client instance.
154
     *
155
     * @return \GuzzleHttp\Client
156
     */
157 4
    public function getClient()
158
    {
159 4
        return $this->client;
160
    }
161
162
    /**
163
     * Get whether to catch Guzzle exceptions or not.
164
     *
165
     * @return bool
166
     */
167 1
    public function areExceptionsCaught()
168
    {
169 1
        return $this->catchExceptions;
170
    }
171
172
    /**
173
     * Set whether to catch Guzzle exceptions or not.
174
     *
175
     * @param  bool  $catch
176
     * @return $this
177
     */
178 2
    public function catchExceptions($catch)
179
    {
180 2
        $this->catchExceptions = (bool) $catch;
181
182 2
        return $this;
183
    }
184
185
    /**
186
     * Get the request options using "dot" notation.
187
     *
188
     * @param  string|null  $key
189
     * @return mixed
190
     */
191 12
    public function getOption($key = null)
192
    {
193 12
        return Arr::get($this->options, $key);
194
    }
195
196
    /**
197
     * Set the request options using "dot" notation.
198
     *
199
     * @param  string|array  $key
200
     * @param  mixed  $value
201
     * @return $this
202
     */
203 9
    public function option($key, $value = null)
204
    {
205 9
        $keys = is_array($key) ? $key : [$key => $value];
206
207 9
        foreach ($keys as $key => $value) {
208 9
            Arr::set($this->options, $key, $value);
209 9
        }
210
211 9
        return $this;
212
    }
213
214
    /**
215
     * Merge the given options to the request options.
216
     *
217
     * @param  array  ...$options
218
     * @return $this
219
     */
220 1
    public function mergeOptions(array ...$options)
221
    {
222 1
        $this->options = array_replace_recursive($this->options, ...$options);
223
224 1
        return $this;
225
    }
226
227
    /**
228
     * Remove one or many request options using "dot" notation.
229
     *
230
     * @param  array|string  $keys
231
     * @return $this
232
     */
233 2
    public function removeOption($keys)
234
    {
235 2
        Arr::forget($this->options, is_array($keys) ? $keys : func_get_args());
236
237 2
        return $this;
238
    }
239
240
    /**
241
     * Set a request header.
242
     *
243
     * @param  string  $name
244
     * @param  mixed  $value
245
     * @return $this
246
     */
247 6
    public function header($name, $value)
248
    {
249 6
        return $this->option('headers.'.$name, $value);
250
    }
251
252
    /**
253
     * Set the request accept type.
254
     *
255
     * @param  string  $type
256
     * @return $this
257
     */
258 3
    public function accept($type)
259
    {
260 3
        return $this->header('Accept', $type);
261
    }
262
263
    /**
264
     * Set the request accept type to "application/json".
265
     *
266
     * @return $this
267
     */
268 1
    public function acceptJson()
269
    {
270 1
        return $this->accept('application/json');
271
    }
272
273
    /**
274
     * Set user agent for the request.
275
     *
276
     * @param  string  $value
277
     * @return $this
278
     */
279 1
    public function userAgent($value)
280
    {
281 1
        return $this->header('User-Agent', $value);
282
    }
283
284
    /**
285
     * Set the request content type.
286
     *
287
     * @param  string  $type
288
     * @return $this
289
     */
290 1
    public function contentType($type)
291
    {
292 1
        return $this->header('Content-Type', $type);
293
    }
294
295
    /**
296
     * Specify where the body of the response will be saved.
297
     * Set the "sink" option.
298
     *
299
     * @param  string|resource|\Psr\Http\Message\StreamInterface  $dest
300
     * @return $this
301
     */
302 1
    public function saveTo($dest)
303
    {
304 1
        return $this->removeOption('save_to')->option('sink', $dest);
305
    }
306
307
    /**
308
     * Get the Guzzle response instance.
309
     *
310
     * @return \GuzzleHttp\Psr7\Response|null
311
     */
312 3
    public function getResponse()
313
    {
314 3
        return $this->response;
315
    }
316
317
    /**
318
     * Get data from the response.
319
     *
320
     * @param  string|\Closure  $callback
321
     * @param  array  $parameters
322
     * @param  mixed  $default
323
     * @return mixed
324
     */
325 5
    protected function getResponseData($callback, array $parameters = [], $default = null)
326
    {
327 5
        if ($this->response) {
328
            return $callback instanceof Closure
329 5
                ? $callback($this->response, ...$parameters)
330 5
                : $this->response->$callback(...$parameters);
331
        }
332
333
        return $default;
334
    }
335
336
    /**
337
     * Get the response content.
338
     *
339
     * @return string
340
     */
341 4
    public function getContent()
342
    {
343 4
        return (string) $this->getBody();
344
    }
345
346
    /**
347
     * Get the JSON-decoded response content.
348
     *
349
     * @param  bool  $assoc
350
     * @return mixed
351
     */
352 2
    public function getJsonContent($assoc = true)
353
    {
354 2
        return json_decode($this->getContent(), $assoc);
355
    }
356
357
    /**
358
     * Make request to a URI.
359
     *
360
     * @param  string|\Psr\Http\Message\UriInterface  $uri
361
     * @param  string  $method
362
     * @param  array  $options
363
     * @return $this
364
     */
365 10
    public function request($uri = '', $method = 'GET', array $options = [])
366
    {
367 10
        $this->response = null;
368
369 10
        $method = strtoupper($method);
370 10
        $options = array_replace_recursive($this->options, $options);
371
372
        try {
373 10
            $this->response = $this->client->request($method, $uri, $options);
0 ignored issues
show
Bug introduced by
It seems like $uri defined by parameter $uri on line 365 can also be of type object<Psr\Http\Message\UriInterface>; however, GuzzleHttp\Client::request() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
374 10
        } catch (Exception $e) {
375 1
            if (! $this->catchExceptions) {
376 1
                throw $e;
377
            }
378
        }
379
380 10
        return $this;
381
    }
382
383
    /**
384
     * Make request to a URI, expecting JSON content.
385
     *
386
     * @param  string|\Psr\Http\Message\UriInterface  $uri
387
     * @param  string  $method
388
     * @param  array  $options
389
     * @return $this
390
     */
391 4
    public function requestJson($uri = '', $method = 'GET', array $options = [])
392
    {
393 4
        $options = $this->addAcceptableJsonType(
394 4
            array_replace_recursive($this->options, $options)
395 4
        );
396
397 4
        return $this->request($uri, $method, $options);
398
    }
399
400
    /**
401
     * Add JSON type to the "Accept" header for the request options.
402
     *
403
     * @param  array  $options
404
     * @return array
405
     */
406 4
    protected function addAcceptableJsonType(array $options)
407
    {
408 4
        $accept = Arr::get($options, 'headers.Accept', '');
409
410 4
        if (! Str::contains($accept, ['/json', '+json'])) {
411 4
            $accept = rtrim('application/json,'.$accept, ',');
412 4
            Arr::set($options, 'headers.Accept', $accept);
413 4
        }
414
415 4
        return $options;
416
    }
417
418
    /**
419
     * Request the URI and return the response content.
420
     *
421
     * @param  string|\Psr\Http\Message\UriInterface  $uri
422
     * @param  string  $method
423
     * @param  array  $options
424
     * @return string
425
     */
426 1
    public function fetchContent($uri = '', $method = 'GET', array $options = [])
427
    {
428 1
        return $this->request($uri, $method, $options)->getContent();
429
    }
430
431
    /**
432
     * Request the URI and return the JSON-decoded response content.
433
     *
434
     * @param  string|\Psr\Http\Message\UriInterface  $uri
435
     * @param  string  $method
436
     * @param  array  $options
437
     * @return mixed
438
     */
439 1
    public function fetchJson($uri = '', $method = 'GET', array $options = [])
440
    {
441 1
        return $this->requestJson($uri, $method, $options)->getJsonContent();
442
    }
443
444
    /**
445
     * Get all allowed magic request methods.
446
     *
447
     * @return array
448
     */
449 9
    protected function getMagicRequestMethods()
450
    {
451
        return [
452 9
            'get', 'head', 'post', 'put', 'patch', 'delete', 'options',
453 9
        ];
454
    }
455
456
    /**
457
     * Determine if the given method is a magic request method.
458
     *
459
     * @param  string  $method
460
     * @param  string  &$requestMethod
461
     * @param  string  &$httpMethod
462
     * @return bool
463
     */
464 9
    protected function isMagicRequestMethod($method, &$requestMethod, &$httpMethod)
465
    {
466 9
        if (strlen($method) > 4 && $pos = strrpos($method, 'Json', -4)) {
467 2
            $httpMethod = substr($method, 0, $pos);
468 2
            $requestMethod = 'requestJson';
469 2
        } else {
470 9
            $httpMethod = $method;
471 9
            $requestMethod = 'request';
472
        }
473
474 9
        if (in_array($httpMethod, $this->getMagicRequestMethods())) {
475 2
            return true;
476
        }
477
478 7
        $httpMethod = $requestMethod = null;
479
480 7
        return false;
481
    }
482
483
    /**
484
     * Get parameters for $this->request() from the magic request methods.
485
     *
486
     * @param  string  $httpMethod
487
     * @param  array  $parameters
488
     * @return array
489
     */
490 2
    protected function getRequestParameters($httpMethod, array $parameters)
491
    {
492 2
        if (empty($parameters)) {
493 1
            $parameters = ['', $httpMethod];
494 1
        } else {
495 2
            array_splice($parameters, 1, 0, $httpMethod);
496
        }
497
498 2
        return $parameters;
499
    }
500
501
    /**
502
     * Get all allowed magic response methods.
503
     *
504
     * @return array
505
     */
506 7
    protected function getMagicResponseMethods()
507
    {
508
        return [
509 7
            'getStatusCode', 'getReasonPhrase', 'getProtocolVersion',
510 7
            'getHeaders', 'hasHeader', 'getHeader', 'getHeaderLine', 'getBody',
511 7
        ];
512
    }
513
514
    /**
515
     * Get all allowed magic option methods.
516
     *
517
     * @return array
518
     */
519 2
    protected function getMagicOptionMethods()
520
    {
521 2
        static $optionMethods = null;
522
523 2
        if (is_null($optionMethods)) {
524 1
            $reflector = new ReflectionClass(RequestOptions::class);
525 1
            $options = array_values(array_diff($reflector->getConstants(), [
526 1
                'synchronous',
527 1
            ]));
528 1
            $optionMethods = array_map([Str::class, 'camel'], $options);
529 1
        }
530
531 2
        return $optionMethods;
532
    }
533
534
    /**
535
     * Get the option key for the given magic option method.
536
     *
537
     * @param  string  $method
538
     * @return string|null
539
     */
540 2
    protected function getOptionKeyForMethod($method)
541
    {
542 2
        if (in_array($method, $this->getMagicOptionMethods())) {
543 1
            return Str::snake($method);
544
        }
545 1
    }
546
547
    /**
548
     * Handle magic method to send request, get response data, or set
549
     * request options.
550
     *
551
     * @param  string  $method
552
     * @param  array  $parameters
553
     * @return mixed
554
     *
555
     * @throws \InvalidArgumentException
556
     * @throws \BadMethodCallException
557
     */
558 9
    public function __call($method, $parameters)
559
    {
560 9
        if ($this->isMagicRequestMethod($method, $request, $httpMethod)) {
561 2
            return $this->{$request}(
562 2
                ...$this->getRequestParameters($httpMethod, $parameters)
563 2
            );
564
        }
565
566 7
        if (in_array($method, $this->getMagicResponseMethods())) {
567 5
            return $this->getResponseData($method, $parameters);
568
        }
569
570 2
        if ($option = $this->getOptionKeyForMethod($method)) {
571 1
            if (empty($parameters)) {
572 1
                throw new InvalidArgumentException("Method [$method] needs one argument.");
573
            }
574
575 1
            return $this->option($option, $parameters[0]);
576
        }
577
578 1
        throw new BadMethodCallException("Method [$method] does not exist.");
579
    }
580
}
581