Completed
Push — master ( 235d4c...500e34 )
by Elf
33:29
created

HttpClient::getMergedOptions()   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 1
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 \GuzzleHttp\Promise\PromiseInterface getAsync(string|UriInterface $uri = '', array $options = [])
25
 * @method \GuzzleHttp\Promise\PromiseInterface headAsync(string|UriInterface $uri = '', array $options = [])
26
 * @method \GuzzleHttp\Promise\PromiseInterface postAsync(string|UriInterface $uri = '', array $options = [])
27
 * @method \GuzzleHttp\Promise\PromiseInterface putAsync(string|UriInterface $uri = '', array $options = [])
28
 * @method \GuzzleHttp\Promise\PromiseInterface patchAsync(string|UriInterface $uri = '', array $options = [])
29
 * @method \GuzzleHttp\Promise\PromiseInterface deleteAsync(string|UriInterface $uri = '', array $options = [])
30
 * @method \GuzzleHttp\Promise\PromiseInterface optionsAsync(string|UriInterface $uri = '', array $options = [])
31
 * @method int getStatusCode()
32
 * @method string getReasonPhrase()
33
 * @method string getProtocolVersion()
34
 * @method array getHeaders()
35
 * @method bool hasHeader(string $header)
36
 * @method array getHeader(string $header)
37
 * @method string getHeaderLine(string $header)
38
 * @method \Psr\Http\Message\StreamInterface getBody()
39
 * @method $this allowRedirects(bool|array $value)
40
 * @method $this auth(array|string|null $value)
41
 * @method $this body(string|resource|\Psr\Http\Message\StreamInterface $value)
42
 * @method $this cert(string|array $value)
43
 * @method $this cookies(bool|\GuzzleHttp\Cookie\CookieJarInterface $value)
44
 * @method $this connectTimeout(float $value)
45
 * @method $this debug(bool|resource $value)
46
 * @method $this decodeContent(string|bool $value)
47
 * @method $this delay(int|float $value)
48
 * @method $this expect(bool|int $value)
49
 * @method $this forceIpResolve(string $value)
50
 * @method $this formParams(array $value)
51
 * @method $this headers(array $value)
52
 * @method $this httpErrors(bool $value)
53
 * @method $this json(mixed $value)
54
 * @method $this multipart(array $value)
55
 * @method $this onHeaders(callable $value)
56
 * @method $this onStats(callable $value)
57
 * @method $this progress(callable $value)
58
 * @method $this proxy(string|array $value)
59
 * @method $this query(array|string $value)
60
 * @method $this readTimeout(float $value)
61
 * @method $this sink(string|resource|\Psr\Http\Message\StreamInterface $value)
62
 * @method $this sslKey(string|array $value)
63
 * @method $this stream(bool $value)
64
 * @method $this verify(bool|string $value)
65
 * @method $this timeout(float $value)
66
 * @method $this version(float|string $value)
67
 *
68
 * @see http://docs.guzzlephp.org/en/stable/request-options.html Request Options
69
 */
70
class HttpClient
71
{
72
    /**
73
     * The default request options.
74
     *
75
     * @var array
76
     */
77
    protected static $defaultOptions = [
78
        'catch_exceptions' => true,
79
        'http_errors' => false,
80
        'connect_timeout' => 5,
81
        'timeout' => 20,
82
    ];
83
84
    /**
85
     * The Guzzle client.
86
     *
87
     * @var \GuzzleHttp\Client
88
     */
89
    protected $client;
90
91
    /**
92
     * The request options.
93
     *
94
     * @var array
95
     */
96
    protected $options = [];
97
98
    /**
99
     * The Guzzle response.
100
     *
101
     * @var \GuzzleHttp\Psr7\Response
102
     */
103
    protected $response;
104
105
    /**
106
     * Get the default request options.
107
     *
108
     * @return array
109
     */
110 128
    public static function defaultOptions()
111
    {
112 128
        return static::$defaultOptions;
113
    }
114
115
    /**
116
     * Set the default request options.
117
     *
118
     * @param  array  $options
119
     * @return void
120
     */
121 128
    public static function setDefaultOptions(array $options)
122
    {
123 128
        static::$defaultOptions = $options;
124 128
    }
125
126
    /**
127
     * Create a new HTTP client instance.
128
     *
129
     * @param  array|string|\Psr\Http\Message\UriInterface  $options  base URI or any request options
130
     * @return static
131
     */
132 8
    public static function create($options = [])
133
    {
134 8
        return new static($options);
135
    }
136
137
    /**
138
     * Create a new HTTP client instance.
139
     *
140
     * @param  array|string|\Psr\Http\Message\UriInterface  $options  base URI or any request options
141
     *
142
     * @throws \InvalidArgumentException
143
     */
144 132
    public function __construct($options = [])
145
    {
146 132
        if (is_string($options) || $options instanceof UriInterface) {
147 8
            $options = ['base_uri' => $options];
148 130
        } elseif (! is_array($options)) {
149 4
            throw new InvalidArgumentException('Options must be a string, UriInterface, or an array');
150
        }
151
152 128
        $this->client = new Client(
153 128
            array_replace_recursive(static::defaultOptions(), $options)
154 64
        );
155
156 128
        $this->options = $this->client->getConfig();
157 128
    }
158
159
    /**
160
     * Get the Guzzle client instance.
161
     *
162
     * @return \GuzzleHttp\Client
163
     */
164 24
    public function getClient()
165
    {
166 24
        return $this->client;
167
    }
168
169
    /**
170
     * Get the request options using "dot" notation.
171
     *
172
     * @param  string|null  $key
173
     * @param  mixed  $default
174
     * @return mixed
175
     */
176 64
    public function getOption($key = null, $default = null)
177
    {
178 64
        return Arr::get($this->options, $key, $default);
179
    }
180
181
    /**
182
     * Set the request options using "dot" notation.
183
     *
184
     * @param  string|array  $key
185
     * @param  mixed  $value
186
     * @return $this
187
     */
188 48
    public function option($key, $value = null)
189
    {
190 48
        $keys = is_array($key) ? $key : [$key => $value];
191
192 48
        foreach ($keys as $key => $value) {
193 48
            Arr::set($this->options, $key, $value);
194 24
        }
195
196 48
        return $this;
197
    }
198
199
    /**
200
     * Merge the given options to the request options.
201
     *
202
     * @param  array  $options
203
     * @return $this
204
     */
205 4
    public function mergeOptions(array $options)
206
    {
207 4
        $this->options = array_replace_recursive($this->options, $options);
208
209 4
        return $this;
210
    }
211
212
    /**
213
     * Remove one or many request options using "dot" notation.
214
     *
215
     * @param  array|string  $keys
216
     * @return $this
217
     */
218 60
    public function removeOption($keys)
219
    {
220 60
        Arr::forget($this->options, is_array($keys) ? $keys : func_get_args());
221
222 60
        return $this;
223
    }
224
225
    /**
226
     * Remove options which related to the request body, e.g. "body",
227
     * "form_params", "json".
228
     *
229
     * @return $this
230
     */
231 52
    public function removeBodyOptions()
232
    {
233 52
        return $this->removeOption([
234 52
            'body', 'form_params', 'multipart', 'json', 'query',
235 26
            'headers.Content-Type',
236 26
        ]);
237
    }
238
239
    /**
240
     * Set a request header.
241
     *
242
     * @param  string  $name
243
     * @param  mixed  $value
244
     * @return $this
245
     */
246 20
    public function header($name, $value)
247
    {
248 20
        return $this->option('headers.'.$name, $value);
249
    }
250
251
    /**
252
     * Remove one or many request headers.
253
     *
254
     * @param  array|string  $names
255
     * @return $this
256
     */
257 4
    public function removeHeader($names)
258
    {
259 4
        if (is_array($headers = $this->getOption('headers'))) {
260 4
            $names = is_array($names) ? $names : func_get_args();
261 4
            $this->option('headers', Arr::except($headers, $names));
262 2
        }
263
264 4
        return $this;
265
    }
266
267
    /**
268
     * Set the request accept type.
269
     *
270
     * @param  string  $type
271
     * @return $this
272
     */
273 8
    public function accept($type)
274
    {
275 8
        return $this->header('Accept', $type);
276
    }
277
278
    /**
279
     * Set the request accept type to "application/json".
280
     *
281
     * @return $this
282
     */
283 4
    public function acceptJson()
284
    {
285 4
        return $this->accept('application/json');
286
    }
287
288
    /**
289
     * Set user agent for the request.
290
     *
291
     * @param  string  $value
292
     * @return $this
293
     */
294 4
    public function userAgent($value)
295
    {
296 4
        return $this->header('User-Agent', $value);
297
    }
298
299
    /**
300
     * Set the request content type.
301
     *
302
     * @param  string  $type
303
     * @return $this
304
     */
305 4
    public function contentType($type)
306
    {
307 4
        return $this->header('Content-Type', $type);
308
    }
309
310
    /**
311
     * Specify where the body of the response will be saved.
312
     * Set the "sink" option.
313
     *
314
     * @param  string|resource|\Psr\Http\Message\StreamInterface  $dest
315
     * @return $this
316
     */
317 4
    public function saveTo($dest)
318
    {
319 4
        return $this->removeOption('save_to')->option('sink', $dest);
320
    }
321
322
    /**
323
     * Determine whether to catch Guzzle exceptions.
324
     *
325
     * @return bool
326
     */
327 8
    public function areExceptionsCaught()
328
    {
329 8
        return $this->getOption('catch_exceptions', false);
330
    }
331
332
    /**
333
     * Set whether to catch Guzzle exceptions or not.
334
     *
335
     * @param  bool  $catch
336
     * @return $this
337
     */
338 8
    public function catchExceptions($catch)
339
    {
340 8
        return $this->option('catch_exceptions', (bool) $catch);
341
    }
342
343
    /**
344
     * Get the Guzzle response instance.
345
     *
346
     * @return \GuzzleHttp\Psr7\Response|null
347
     */
348 20
    public function getResponse()
349
    {
350 20
        return $this->response;
351
    }
352
353
    /**
354
     * Get data from the response.
355
     *
356
     * @param  string|\Closure  $callback
357
     * @param  mixed  $parameters
358
     * @param  mixed  $default
359
     * @return mixed
360
     */
361 24
    public function getResponseData($callback, $parameters = [], $default = null)
362
    {
363 24
        if ($this->response) {
364 12
            return $callback instanceof Closure
365 14
                ? $callback($this->response, ...(array) $parameters)
366 24
                : $this->response->$callback(...(array) $parameters);
367
        }
368
369 4
        return $default;
370
    }
371
372
    /**
373
     * Get the response content.
374
     *
375
     * @return string
376
     */
377 16
    public function getContent()
378
    {
379 16
        return (string) $this->getBody();
380
    }
381
382
    /**
383
     * Get the JSON-decoded response content.
384
     *
385
     * @param  bool  $assoc
386
     * @return mixed
387
     */
388 8
    public function getJson($assoc = true)
389
    {
390 8
        return json_decode($this->getContent(), $assoc);
391
    }
392
393
    /**
394
     * Send request to a URI.
395
     *
396
     * @param  string|\Psr\Http\Message\UriInterface  $uri
397
     * @param  string  $method
398
     * @param  array  $options
399
     * @return $this
400
     */
401 48
    public function request($uri = '', $method = 'GET', array $options = [])
402
    {
403 48
        $this->response = null;
404
405
        try {
406 48
            $this->response = $this->client->request(
407 48
                strtoupper($method), $uri, $this->getRequestOptions($options)
408 24
            );
409 26
        } catch (Exception $e) {
410 4
            if (! $this->areExceptionsCaught()) {
411 4
                throw $e;
412
            }
413
        }
414
415 48
        return $this;
416
    }
417
418
    /**
419
     * Get options for the Guzzle request method.
420
     *
421
     * @param  array  $options
422
     * @return array
423
     */
424 52
    protected function getRequestOptions(array $options = [])
425
    {
426 52
        $options = array_replace_recursive($this->options, $options);
427
428 52
        $this->removeBodyOptions();
429
430 52
        return $options;
431
    }
432
433
    /**
434
     * Send request to a URI, expecting JSON content.
435
     *
436
     * @param  string|\Psr\Http\Message\UriInterface  $uri
437
     * @param  string  $method
438
     * @param  array  $options
439
     * @return $this
440
     */
441 8
    public function requestJson($uri = '', $method = 'GET', array $options = [])
442
    {
443 8
        Arr::set($options, 'headers.Accept', 'application/json');
444
445 8
        return $this->request($uri, $method, $options);
446
    }
447
448
    /**
449
     * Send asynchronous request to a URI.
450
     *
451
     * @param  string|\Psr\Http\Message\UriInterface  $uri
452
     * @param  string  $method
453
     * @param  array  $options
454
     * @return \GuzzleHttp\Promise\PromiseInterface
455
     */
456 8
    public function requestAsync($uri = '', $method = 'GET', array $options = [])
457
    {
458 8
        return $this->client->requestAsync(
459 8
            strtoupper($method), $uri, $this->getRequestOptions($options)
460 4
        );
461
    }
462
463
    /**
464
     * Request the URI and return the response content.
465
     *
466
     * @param  string|\Psr\Http\Message\UriInterface  $uri
467
     * @param  string  $method
468
     * @param  array  $options
469
     * @return string
470
     */
471 4
    public function fetchContent($uri = '', $method = 'GET', array $options = [])
472
    {
473 4
        return $this->request($uri, $method, $options)->getContent();
474
    }
475
476
    /**
477
     * Request the URI and return the JSON-decoded response content.
478
     *
479
     * @param  string|\Psr\Http\Message\UriInterface  $uri
480
     * @param  string  $method
481
     * @param  array  $options
482
     * @return mixed
483
     */
484 4
    public function fetchJson($uri = '', $method = 'GET', array $options = [])
485
    {
486 4
        return $this->requestJson($uri, $method, $options)->getJson();
487
    }
488
489
    /**
490
     * Get all allowed magic request methods.
491
     *
492
     * @return array
493
     */
494 36
    protected function getMagicRequestMethods()
495
    {
496
        return [
497 36
            'get', 'head', 'post', 'put', 'patch', 'delete', 'options',
498 18
        ];
499
    }
500
501
    /**
502
     * Determine if the given method is a magic request method.
503
     *
504
     * @param  string  $method
505
     * @param  string  &$requestMethod
506
     * @param  string  &$httpMethod
507
     * @return bool
508
     */
509 36
    protected function isMagicRequestMethod($method, &$requestMethod, &$httpMethod)
510
    {
511 36
        if (strlen($method) > 5 && $pos = strrpos($method, 'Async', -5)) {
512 4
            $httpMethod = substr($method, 0, $pos);
513 4
            $requestMethod = 'requestAsync';
514 2
        } else {
515 36
            $httpMethod = $method;
516 36
            $requestMethod = 'request';
517
        }
518
519 36
        if (! in_array($httpMethod, $this->getMagicRequestMethods())) {
520 28
            $httpMethod = $requestMethod = null;
521 14
        }
522
523 36
        return (bool) $httpMethod;
524
    }
525
526
    /**
527
     * Get parameters for the request() method from the magic request call.
528
     *
529
     * @param  string  $method
530
     * @param  array  $parameters
531
     * @return array
532
     */
533 8
    protected function getRequestParameters($method, array $parameters)
534
    {
535 8
        if (empty($parameters)) {
536 4
            $parameters = ['', $method];
537 2
        } else {
538 8
            array_splice($parameters, 1, 0, $method);
539
        }
540
541 8
        return $parameters;
542
    }
543
544
    /**
545
     * Get all allowed magic response methods.
546
     *
547
     * @return array
548
     */
549 28
    protected function getMagicResponseMethods()
550
    {
551
        return [
552 28
            'getStatusCode', 'getReasonPhrase', 'getProtocolVersion',
553 14
            'getHeaders', 'hasHeader', 'getHeader', 'getHeaderLine', 'getBody',
554 14
        ];
555
    }
556
557
    /**
558
     * Determine if the given method is a magic response method.
559
     *
560
     * @param  string  $method
561
     * @return bool
562
     */
563 28
    protected function isMagicResponseMethod($method)
564
    {
565 28
        return in_array($method, $this->getMagicResponseMethods());
566
    }
567
568
    /**
569
     * Get all allowed magic option methods.
570
     *
571
     * @return array
572
     */
573 8
    protected function getMagicOptionMethods()
574
    {
575 8
        static $optionMethods = null;
576
577 8
        if (is_null($optionMethods)) {
578 4
            $reflector = new ReflectionClass(RequestOptions::class);
579 4
            $optionMethods = array_map(
580 4
                [Str::class, 'camel'],
581 4
                array_values($reflector->getConstants())
582 2
            );
583 2
        }
584
585 8
        return $optionMethods;
586
    }
587
588
    /**
589
     * Determine if the given method is a magic option method.
590
     *
591
     * @param  string  $method
592
     * @return bool
593
     */
594 8
    protected function isMagicOptionMethod($method, &$option)
595
    {
596 8
        $option = in_array($method, $this->getMagicOptionMethods())
597 8
            ? Str::snake($method) : null;
598
599 8
        return (bool) $option;
600
    }
601
602
    /**
603
     * Handle magic method to send request, get response data, or set
604
     * request options.
605
     *
606
     * @param  string  $method
607
     * @param  array  $parameters
608
     * @return mixed
609
     *
610
     * @throws \InvalidArgumentException
611
     * @throws \BadMethodCallException
612
     */
613 36
    public function __call($method, $parameters)
614
    {
615 36
        if ($this->isMagicRequestMethod($method, $requestMethod, $httpMethod)) {
616 8
            return $this->$requestMethod(...$this->getRequestParameters($httpMethod, $parameters));
617
        }
618
619 28
        if ($this->isMagicResponseMethod($method)) {
620 20
            return $this->getResponseData($method, $parameters);
621
        }
622
623 8
        if ($this->isMagicOptionMethod($method, $option)) {
624 4
            if (empty($parameters)) {
625 4
                throw new InvalidArgumentException("Method [$method] needs one argument.");
626
            }
627
628 4
            return $this->option($option, $parameters[0]);
629
        }
630
631 4
        throw new BadMethodCallException("Method [$method] does not exist.");
632
    }
633
}
634