Passed
Push — master ( e0ee4b...07d423 )
by Elf
01:38
created

HttpClient::getClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 int getStatusCode()
25
 * @method string getReasonPhrase()
26
 * @method string getProtocolVersion()
27
 * @method array getHeaders()
28
 * @method bool hasHeader(string $header)
29
 * @method array getHeader(string $header)
30
 * @method string getHeaderLine(string $header)
31
 * @method \Psr\Http\Message\StreamInterface getBody()
32
 * @method $this allowRedirects(bool|array $value)
33
 * @method $this auth(array|string|null $value)
34
 * @method $this body(mixed $value)
35
 * @method $this cert(string|array $value)
36
 * @method $this cookies(bool|\GuzzleHttp\Cookie\CookieJarInterface $value)
37
 * @method $this connectTimeout(float $value)
38
 * @method $this debug(bool|resource $value)
39
 * @method $this decodeContent(bool $value)
40
 * @method $this delay(int|float $value)
41
 * @method $this expect(bool|int $value)
42
 * @method $this formParams(array $value)
43
 * @method $this headers(array $value)
44
 * @method $this httpErrors(bool $value)
45
 * @method $this json(mixed $value)
46
 * @method $this multipart(array $value)
47
 * @method $this onHeaders(callable $value)
48
 * @method $this onStats(callable $value)
49
 * @method $this progress(callable $value)
50
 * @method $this proxy(string|array $value)
51
 * @method $this query(array|string $value)
52
 * @method $this sink(string|resource|\Psr\Http\Message\StreamInterface $value)
53
 * @method $this sslKey(array|string $value)
54
 * @method $this stream(bool $value)
55
 * @method $this verify(bool|string $value)
56
 * @method $this timeout(float $value)
57
 * @method $this readTimeout(float $value)
58
 * @method $this version(float|string $value)
59
 * @method $this forceIpResolve(string $value)
60
 *
61
 * @see http://docs.guzzlephp.org/en/stable/request-options.html Request Options
62
 */
63
class HttpClient
64
{
65
    /**
66
     * The default request options.
67
     *
68
     * @var array
69
     */
70
    protected static $defaultOptions = [
71
        'connect_timeout' => 5,
72
        'timeout' => 20,
73
        'http_errors' => false,
74
    ];
75
76
    /**
77
     * The Guzzle client.
78
     *
79
     * @var \GuzzleHttp\Client
80
     */
81
    protected $client;
82
83
    /**
84
     * The request options.
85
     *
86
     * @var array
87
     */
88
    protected $options = [];
89
90
    /**
91
     * The Guzzle response.
92
     *
93
     * @var \GuzzleHttp\Psr7\Response
94
     */
95
    protected $response;
96
97
    /**
98
     * Indicate whether to catch Guzzle exceptions.
99
     *
100
     * @var bool
101
     */
102
    protected $catchExceptions = true;
103
104
    /**
105
     * Get the default request options.
106
     *
107
     * @return array
108
     */
109 54
    public static function defaultOptions()
110
    {
111 54
        return static::$defaultOptions;
112
    }
113
114
    /**
115
     * Set the default request options.
116
     *
117
     * @param  array  $options
118
     * @return void
119
     */
120 56
    public static function setDefaultOptions(array $options)
121
    {
122 56
        static::$defaultOptions = $options;
123 56
    }
124
125
    /**
126
     * Create a new http client instance.
127
     *
128
     * @param  array|string|\Psr\Http\Message\UriInterface  $options  base URI or any request options
129
     *
130
     * @throws \InvalidArgumentException
131
     */
132 54
    public function __construct($options = [])
133
    {
134 54
        if (is_string($options) || $options instanceof UriInterface) {
135 2
            $options = ['base_uri' => $options];
136 52
        } elseif (! is_array($options)) {
137 2
            throw new InvalidArgumentException('Options must be a string, UriInterface, or an array');
138
        }
139
140 52
        $this->client = new Client(
141 52
            array_replace_recursive(static::defaultOptions(), $options)
142
        );
143
144 52
        $this->options = $this->client->getConfig();
145 52
    }
146
147
    /**
148
     * Get the Guzzle client instance.
149
     *
150
     * @return \GuzzleHttp\Client
151
     */
152 8
    public function getClient()
153
    {
154 8
        return $this->client;
155
    }
156
157
    /**
158
     * Get whether to catch Guzzle exceptions or not.
159
     *
160
     * @return bool
161
     */
162 2
    public function areExceptionsCaught()
163
    {
164 2
        return $this->catchExceptions;
165
    }
166
167
    /**
168
     * Set whether to catch Guzzle exceptions or not.
169
     *
170
     * @param  bool  $catch
171
     * @return $this
172
     */
173 4
    public function catchExceptions($catch)
174
    {
175 4
        $this->catchExceptions = (bool) $catch;
176
177 4
        return $this;
178
    }
179
180
    /**
181
     * Get the request options using "dot" notation.
182
     *
183
     * @param  string|null  $key
184
     * @return mixed
185
     */
186 24
    public function getOption($key = null)
187
    {
188 24
        return Arr::get($this->options, $key);
189
    }
190
191
    /**
192
     * Set the request options using "dot" notation.
193
     *
194
     * @param  string|array  $key
195
     * @param  mixed  $value
196
     * @return $this
197
     */
198 18
    public function option($key, $value = null)
199
    {
200 18
        $keys = is_array($key) ? $key : [$key => $value];
201
202 18
        foreach ($keys as $key => $value) {
203 18
            Arr::set($this->options, $key, $value);
204
        }
205
206 18
        return $this;
207
    }
208
209
    /**
210
     * Merge the given options to the request options.
211
     *
212
     * @param  array  ...$options
213
     * @return $this
214
     */
215 2
    public function mergeOptions(array ...$options)
216
    {
217 2
        $this->options = array_replace_recursive($this->options, ...$options);
0 ignored issues
show
Bug introduced by
$options is expanded, but the parameter $array1 of array_replace_recursive() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

217
        $this->options = array_replace_recursive($this->options, /** @scrutinizer ignore-type */ ...$options);
Loading history...
218
219 2
        return $this;
220
    }
221
222
    /**
223
     * Remove one or many request options using "dot" notation.
224
     *
225
     * @param  array|string  $keys
226
     * @return $this
227
     */
228 4
    public function removeOption($keys)
229
    {
230 4
        Arr::forget($this->options, is_array($keys) ? $keys : func_get_args());
231
232 4
        return $this;
233
    }
234
235
    /**
236
     * Set a request header.
237
     *
238
     * @param  string  $name
239
     * @param  mixed  $value
240
     * @return $this
241
     */
242 12
    public function header($name, $value)
243
    {
244 12
        return $this->option('headers.'.$name, $value);
245
    }
246
247
    /**
248
     * Set the request accept type.
249
     *
250
     * @param  string  $type
251
     * @return $this
252
     */
253 6
    public function accept($type)
254
    {
255 6
        return $this->header('Accept', $type);
256
    }
257
258
    /**
259
     * Set the request accept type to "application/json".
260
     *
261
     * @return $this
262
     */
263 2
    public function acceptJson()
264
    {
265 2
        return $this->accept('application/json');
266
    }
267
268
    /**
269
     * Set user agent for the request.
270
     *
271
     * @param  string  $value
272
     * @return $this
273
     */
274 2
    public function userAgent($value)
275
    {
276 2
        return $this->header('User-Agent', $value);
277
    }
278
279
    /**
280
     * Set the request content type.
281
     *
282
     * @param  string  $type
283
     * @return $this
284
     */
285 2
    public function contentType($type)
286
    {
287 2
        return $this->header('Content-Type', $type);
288
    }
289
290
    /**
291
     * Specify where the body of the response will be saved.
292
     * Set the "sink" option.
293
     *
294
     * @param  string|resource|\Psr\Http\Message\StreamInterface  $dest
295
     * @return $this
296
     */
297 2
    public function saveTo($dest)
298
    {
299 2
        return $this->removeOption('save_to')->option('sink', $dest);
300
    }
301
302
    /**
303
     * Get the Guzzle response instance.
304
     *
305
     * @return \GuzzleHttp\Psr7\Response|null
306
     */
307 6
    public function getResponse()
308
    {
309 6
        return $this->response;
310
    }
311
312
    /**
313
     * Get data from the response.
314
     *
315
     * @param  string|\Closure  $callback
316
     * @param  mixed  $parameters
317
     * @param  mixed  $default
318
     * @return mixed
319
     */
320 12
    public function getResponseData($callback, $parameters = [], $default = null)
321
    {
322 12
        if ($this->response) {
323 12
            return $callback instanceof Closure
324 2
                ? $callback($this->response, ...(array) $parameters)
325 12
                : $this->response->$callback(...(array) $parameters);
326
        }
327
328 2
        return $default;
329
    }
330
331
    /**
332
     * Get the response content.
333
     *
334
     * @return string
335
     */
336 8
    public function getContent()
337
    {
338 8
        return (string) $this->getBody();
339
    }
340
341
    /**
342
     * Get the JSON-decoded response content.
343
     *
344
     * @param  bool  $assoc
345
     * @return mixed
346
     */
347 4
    public function getJson($assoc = true)
348
    {
349 4
        return json_decode($this->getContent(), $assoc);
350
    }
351
352
    /**
353
     * Make request to a URI.
354
     *
355
     * @param  string|\Psr\Http\Message\UriInterface  $uri
356
     * @param  string  $method
357
     * @param  array  $options
358
     * @return $this
359
     */
360 20
    public function request($uri = '', $method = 'GET', array $options = [])
361
    {
362 20
        $this->response = null;
363
364 20
        $method = strtoupper($method);
365 20
        $options = array_replace_recursive($this->options, $options);
366
367
        try {
368 20
            $this->response = $this->client->request($method, $uri, $options);
369 2
        } catch (Exception $e) {
370 2
            if (! $this->catchExceptions) {
371 2
                throw $e;
372
            }
373
        }
374
375 20
        return $this;
376
    }
377
378
    /**
379
     * Make request to a URI, expecting JSON content.
380
     *
381
     * @param  string|\Psr\Http\Message\UriInterface  $uri
382
     * @param  string  $method
383
     * @param  array  $options
384
     * @return $this
385
     */
386 4
    public function requestJson($uri = '', $method = 'GET', array $options = [])
387
    {
388 4
        $options = $this->addAcceptableJsonType(
389 4
            array_replace_recursive($this->options, $options)
390
        );
391
392 4
        return $this->request($uri, $method, $options);
393
    }
394
395
    /**
396
     * Add JSON type to the "Accept" header for the request options.
397
     *
398
     * @param  array  $options
399
     * @return array
400
     */
401 4
    protected function addAcceptableJsonType(array $options)
402
    {
403 4
        $accept = Arr::get($options, 'headers.Accept', '');
404
405 4
        if (! Str::contains($accept, ['/json', '+json'])) {
0 ignored issues
show
Bug introduced by
It seems like $accept can also be of type array; however, parameter $haystack of Illuminate\Support\Str::contains() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

405
        if (! Str::contains(/** @scrutinizer ignore-type */ $accept, ['/json', '+json'])) {
Loading history...
406 4
            $accept = rtrim('application/json,'.$accept, ',');
0 ignored issues
show
Bug introduced by
Are you sure $accept of type mixed|string|array can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

406
            $accept = rtrim('application/json,'./** @scrutinizer ignore-type */ $accept, ',');
Loading history...
407 4
            Arr::set($options, 'headers.Accept', $accept);
408
        }
409
410 4
        return $options;
411
    }
412
413
    /**
414
     * Request the URI and return the response content.
415
     *
416
     * @param  string|\Psr\Http\Message\UriInterface  $uri
417
     * @param  string  $method
418
     * @param  array  $options
419
     * @return string
420
     */
421 2
    public function fetchContent($uri = '', $method = 'GET', array $options = [])
422
    {
423 2
        return $this->request($uri, $method, $options)->getContent();
424
    }
425
426
    /**
427
     * Request the URI and return the JSON-decoded response content.
428
     *
429
     * @param  string|\Psr\Http\Message\UriInterface  $uri
430
     * @param  string  $method
431
     * @param  array  $options
432
     * @return mixed
433
     */
434 2
    public function fetchJson($uri = '', $method = 'GET', array $options = [])
435
    {
436 2
        return $this->requestJson($uri, $method, $options)->getJson();
437
    }
438
439
    /**
440
     * Get all allowed magic request methods.
441
     *
442
     * @return array
443
     */
444 16
    protected function getMagicRequestMethods()
445
    {
446
        return [
447 16
            'get', 'head', 'post', 'put', 'patch', 'delete', 'options',
448
        ];
449
    }
450
451
    /**
452
     * Get parameters for $this->request() from the magic request call.
453
     *
454
     * @param  string  $method
455
     * @param  array  $parameters
456
     * @return array
457
     */
458 2
    protected function getRequestParameters($method, array $parameters)
459
    {
460 2
        if (empty($parameters)) {
461
            $parameters = ['', $method];
462
        } else {
463 2
            array_splice($parameters, 1, 0, $method);
464
        }
465
466 2
        return $parameters;
467
    }
468
469
    /**
470
     * Get all allowed magic response methods.
471
     *
472
     * @return array
473
     */
474 14
    protected function getMagicResponseMethods()
475
    {
476
        return [
477 14
            'getStatusCode', 'getReasonPhrase', 'getProtocolVersion',
478
            'getHeaders', 'hasHeader', 'getHeader', 'getHeaderLine', 'getBody',
479
        ];
480
    }
481
482
    /**
483
     * Get all allowed magic option methods.
484
     *
485
     * @return array
486
     */
487 4
    protected function getMagicOptionMethods()
488
    {
489 4
        static $optionMethods = null;
490
491 4
        if (is_null($optionMethods)) {
492 2
            $reflector = new ReflectionClass(RequestOptions::class);
493 2
            $optionMethods = array_map(
494 2
                [Str::class, 'camel'],
495 2
                array_values($reflector->getConstants())
496
            );
497
        }
498
499 4
        return $optionMethods;
500
    }
501
502
    /**
503
     * Get the option name for the given magic option method.
504
     *
505
     * @param  string  $method
506
     * @return string|null
507
     */
508 4
    protected function getOptionNameForMethod($method)
509
    {
510 4
        if (in_array($method, $this->getMagicOptionMethods())) {
511 2
            return Str::snake($method);
512
        }
513 2
    }
514
515
    /**
516
     * Handle magic method to send request, get response data, or set
517
     * request options.
518
     *
519
     * @param  string  $method
520
     * @param  array  $parameters
521
     * @return mixed
522
     *
523
     * @throws \InvalidArgumentException
524
     * @throws \BadMethodCallException
525
     */
526 16
    public function __call($method, $parameters)
527
    {
528 16
        if (in_array($method, $this->getMagicRequestMethods())) {
529 2
            return $this->request(...$this->getRequestParameters($method, $parameters));
0 ignored issues
show
Bug introduced by
$this->getRequestParameters($method, $parameters) is expanded, but the parameter $uri of ElfSundae\HttpClient::request() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

529
            return $this->request(/** @scrutinizer ignore-type */ ...$this->getRequestParameters($method, $parameters));
Loading history...
530
        }
531
532 14
        if (in_array($method, $this->getMagicResponseMethods())) {
533 10
            return $this->getResponseData($method, $parameters);
534
        }
535
536 4
        if ($option = $this->getOptionNameForMethod($method)) {
537 2
            if (empty($parameters)) {
538 2
                throw new InvalidArgumentException("Method [$method] needs one argument.");
539
            }
540
541 2
            return $this->option($option, $parameters[0]);
542
        }
543
544 2
        throw new BadMethodCallException("Method [$method] does not exist.");
545
    }
546
}
547