Completed
Push — master ( 37d3dc...d6e19f )
by Elf
02:29
created

HttpClient::isMagicResponseMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
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(mixed $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(bool $value)
47
 * @method $this delay(int|float $value)
48
 * @method $this expect(bool|int $value)
49
 * @method $this formParams(array $value)
50
 * @method $this headers(array $value)
51
 * @method $this httpErrors(bool $value)
52
 * @method $this json(mixed $value)
53
 * @method $this multipart(array $value)
54
 * @method $this onHeaders(callable $value)
55
 * @method $this onStats(callable $value)
56
 * @method $this progress(callable $value)
57
 * @method $this proxy(string|array $value)
58
 * @method $this query(array|string $value)
59
 * @method $this sink(string|resource|\Psr\Http\Message\StreamInterface $value)
60
 * @method $this sslKey(array|string $value)
61
 * @method $this stream(bool $value)
62
 * @method $this verify(bool|string $value)
63
 * @method $this timeout(float $value)
64
 * @method $this readTimeout(float $value)
65
 * @method $this version(float|string $value)
66
 * @method $this forceIpResolve(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 108
    public static function defaultOptions()
111
    {
112 108
        return static::$defaultOptions;
113
    }
114
115
    /**
116
     * Set the default request options.
117
     *
118
     * @param  array  $options
119
     * @return void
120
     */
121 108
    public static function setDefaultOptions(array $options)
122
    {
123 108
        static::$defaultOptions = $options;
124 108
    }
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 112
    public function __construct($options = [])
145
    {
146 112
        if (is_string($options) || $options instanceof UriInterface) {
147 8
            $options = ['base_uri' => $options];
148 110
        } elseif (! is_array($options)) {
149 4
            throw new InvalidArgumentException('Options must be a string, UriInterface, or an array');
150
        }
151
152 108
        $this->client = new Client(
153 108
            array_replace_recursive(static::defaultOptions(), $options)
154 60
        );
155
156 108
        $this->options = $this->client->getConfig();
157 108
    }
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 52
    public function getOption($key = null, $default = null)
177
    {
178 52
        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 40
    public function option($key, $value = null)
189
    {
190 40
        $keys = is_array($key) ? $key : [$key => $value];
191
192 40
        foreach ($keys as $key => $value) {
193 40
            Arr::set($this->options, $key, $value);
194 20
        }
195
196 40
        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 8
    public function removeOption($keys)
219
    {
220 8
        Arr::forget($this->options, is_array($keys) ? $keys : func_get_args());
221
222 8
        return $this;
223
    }
224
225
    /**
226
     * Determine whether to catch Guzzle exceptions.
227
     *
228
     * @return bool
229
     */
230 8
    public function areExceptionsCaught()
231
    {
232 8
        return $this->getOption('catch_exceptions', false);
233
    }
234
235
    /**
236
     * Set whether to catch Guzzle exceptions or not.
237
     *
238
     * @param  bool  $catch
239
     * @return $this
240
     */
241 8
    public function catchExceptions($catch)
242
    {
243 8
        return $this->option('catch_exceptions', (bool) $catch);
244
    }
245
246
    /**
247
     * Set a request header.
248
     *
249
     * @param  string  $name
250
     * @param  mixed  $value
251
     * @return $this
252
     */
253 24
    public function header($name, $value)
254
    {
255 24
        return $this->option('headers.'.$name, $value);
256
    }
257
258
    /**
259
     * Set the request accept type.
260
     *
261
     * @param  string  $type
262
     * @return $this
263
     */
264 12
    public function accept($type)
265
    {
266 12
        return $this->header('Accept', $type);
267
    }
268
269
    /**
270
     * Set the request accept type to "application/json".
271
     *
272
     * @return $this
273
     */
274 4
    public function acceptJson()
275
    {
276 4
        return $this->accept('application/json');
277
    }
278
279
    /**
280
     * Set user agent for the request.
281
     *
282
     * @param  string  $value
283
     * @return $this
284
     */
285 4
    public function userAgent($value)
286
    {
287 4
        return $this->header('User-Agent', $value);
288
    }
289
290
    /**
291
     * Set the request content type.
292
     *
293
     * @param  string  $type
294
     * @return $this
295
     */
296 4
    public function contentType($type)
297
    {
298 4
        return $this->header('Content-Type', $type);
299
    }
300
301
    /**
302
     * Specify where the body of the response will be saved.
303
     * Set the "sink" option.
304
     *
305
     * @param  string|resource|\Psr\Http\Message\StreamInterface  $dest
306
     * @return $this
307
     */
308 4
    public function saveTo($dest)
309
    {
310 4
        return $this->removeOption('save_to')->option('sink', $dest);
311
    }
312
313
    /**
314
     * Get the Guzzle response instance.
315
     *
316
     * @return \GuzzleHttp\Psr7\Response|null
317
     */
318 16
    public function getResponse()
319
    {
320 16
        return $this->response;
321
    }
322
323
    /**
324
     * Get data from the response.
325
     *
326
     * @param  string|\Closure  $callback
327
     * @param  mixed  $parameters
328
     * @param  mixed  $default
329
     * @return mixed
330
     */
331 4
    public function getResponseData($callback, $parameters = [], $default = null)
332
    {
333 4
        if ($this->response) {
334 2
            return $callback instanceof Closure
335 4
                ? $callback($this->response, ...(array) $parameters)
336 4
                : $this->response->$callback(...(array) $parameters);
337
        }
338
339 4
        return $default;
340
    }
341
342
    /**
343
     * Get the response content.
344
     *
345
     * @return string
346
     */
347 8
    public function getContent()
348
    {
349 8
        return (string) $this->getBody();
350
    }
351
352
    /**
353
     * Get the JSON-decoded response content.
354
     *
355
     * @param  bool  $assoc
356
     * @return mixed
357
     */
358 4
    public function getJson($assoc = true)
359
    {
360 4
        return json_decode($this->getContent(), $assoc);
361
    }
362
363
    /**
364
     * Make request to a URI.
365
     *
366
     * @param  string|\Psr\Http\Message\UriInterface  $uri
367
     * @param  string  $method
368
     * @param  array  $options
369
     * @return $this
370
     */
371 34
    public function request($uri = '', $method = 'GET', array $options = [])
372
    {
373 34
        $this->response = null;
374
375 34
        $method = strtoupper($method);
376 34
        $options = array_replace_recursive($this->options, $options);
377
378
        try {
379 34
            $this->response = $this->client->request($method, $uri, $options);
380 24
        } catch (Exception $e) {
381 4
            if (! $this->areExceptionsCaught()) {
382 4
                throw $e;
383
            }
384
        }
385
386 34
        return $this;
387
    }
388
389
    /**
390
     * Make request to a URI, expecting JSON content.
391
     *
392
     * @param  string|\Psr\Http\Message\UriInterface  $uri
393
     * @param  string  $method
394
     * @param  array  $options
395
     * @return $this
396
     */
397 6
    public function requestJson($uri = '', $method = 'GET', array $options = [])
398
    {
399 6
        $options = $this->addAcceptableJsonType(
400 6
            array_replace_recursive($this->options, $options)
401 4
        );
402
403 6
        return $this->request($uri, $method, $options);
404
    }
405
406
    /**
407
     * Add JSON type to the "Accept" header for the request options.
408
     *
409
     * @param  array  $options
410
     * @return array
411
     */
412 6
    protected function addAcceptableJsonType(array $options)
413
    {
414 6
        $accept = Arr::get($options, 'headers.Accept', '');
415
416 6
        if (! Str::contains($accept, ['/json', '+json'])) {
417 6
            $accept = rtrim('application/json,'.$accept, ',');
418 6
            Arr::set($options, 'headers.Accept', $accept);
419 4
        }
420
421 6
        return $options;
422
    }
423
424
    /**
425
     * Make asynchronous request to a URI.
426
     *
427
     * @param  string|\Psr\Http\Message\UriInterface  $uri
428
     * @param  string  $method
429
     * @param  array  $options
430
     * @return \GuzzleHttp\Promise\PromiseInterface
431
     */
432 8
    public function requestAsync($uri = '', $method = 'GET', array $options = [])
433
    {
434 8
        return $this->client->requestAsync(
435 8
            strtoupper($method),
436 8
            $uri,
437 8
            array_replace_recursive($this->options, $options)
438 4
        );
439
    }
440
441
    /**
442
     * Request the URI and return the response content.
443
     *
444
     * @param  string|\Psr\Http\Message\UriInterface  $uri
445
     * @param  string  $method
446
     * @param  array  $options
447
     * @return string
448
     */
449 2
    public function fetchContent($uri = '', $method = 'GET', array $options = [])
450
    {
451 2
        return $this->request($uri, $method, $options)->getContent();
452
    }
453
454
    /**
455
     * Request the URI and return the JSON-decoded response content.
456
     *
457
     * @param  string|\Psr\Http\Message\UriInterface  $uri
458
     * @param  string  $method
459
     * @param  array  $options
460
     * @return mixed
461
     */
462 2
    public function fetchJson($uri = '', $method = 'GET', array $options = [])
463
    {
464 2
        return $this->requestJson($uri, $method, $options)->getJson();
465
    }
466
467
    /**
468
     * Get all allowed magic request methods.
469
     *
470
     * @return array
471
     */
472 24
    protected function getMagicRequestMethods()
473
    {
474
        return [
475 24
            'get', 'head', 'post', 'put', 'patch', 'delete', 'options',
476 18
        ];
477
    }
478
479
    /**
480
     * Determine if the given method is a magic request method.
481
     *
482
     * @param  string  $method
483
     * @param  string  &$requestMethod
484
     * @param  string  &$httpMethod
485
     * @return bool
486
     */
487 24
    protected function isMagicRequestMethod($method, &$requestMethod, &$httpMethod)
488
    {
489 24
        if (strlen($method) > 5 && $pos = strrpos($method, 'Async', -5)) {
490 4
            $httpMethod = substr($method, 0, $pos);
491 4
            $requestMethod = 'requestAsync';
492 2
        } else {
493 24
            $httpMethod = $method;
494 24
            $requestMethod = 'request';
495
        }
496
497 24
        if (in_array($httpMethod, $this->getMagicRequestMethods())) {
498 8
            return true;
499
        }
500
501 16
        $httpMethod = $requestMethod = null;
502
503 16
        return false;
504
    }
505
506
    /**
507
     * Get parameters for $this->request() from the magic request call.
508
     *
509
     * @param  string  $method
510
     * @param  array  $parameters
511
     * @return array
512
     */
513 8
    protected function getRequestParameters($method, array $parameters)
514
    {
515 8
        if (empty($parameters)) {
516 4
            $parameters = ['', $method];
517 2
        } else {
518 8
            array_splice($parameters, 1, 0, $method);
519
        }
520
521 8
        return $parameters;
522
    }
523
524
    /**
525
     * Get all allowed magic response methods.
526
     *
527
     * @return array
528
     */
529
    protected function getMagicResponseMethods()
530
    {
531
        return [
532
            'getStatusCode', 'getReasonPhrase', 'getProtocolVersion',
533
            'getHeaders', 'hasHeader', 'getHeader', 'getHeaderLine', 'getBody',
534
        ];
535
    }
536
537
    /**
538
     * Determine if the given method is a magic response method.
539
     *
540
     * @param  string  $method
541
     * @return bool
542
     */
543
    protected function isMagicResponseMethod($method)
544
    {
545
        return in_array($method, $this->getMagicResponseMethods());
546
    }
547
548
    /**
549
     * Get all allowed magic option methods.
550
     *
551
     * @return array
552
     */
553 2
    protected function getMagicOptionMethods()
554
    {
555 2
        static $optionMethods = null;
556
557 2
        if (is_null($optionMethods)) {
558 2
            $reflector = new ReflectionClass(RequestOptions::class);
559 2
            $optionMethods = array_map(
560 2
                [Str::class, 'camel'],
561 2
                array_values($reflector->getConstants())
562 2
            );
563 2
        }
564
565 2
        return $optionMethods;
566
    }
567
568
    /**
569
     * Get the option name for the given magic option method.
570
     *
571
     * @param  string  $method
572
     * @return string|null
573
     */
574
    protected function getOptionNameForMethod($method)
575
    {
576
        if (in_array($method, $this->getMagicOptionMethods())) {
577
            return Str::snake($method);
578
        }
579
    }
580
581
    /**
582
     * Handle magic method to send request, get response data, or set
583
     * request options.
584
     *
585
     * @param  string  $method
586
     * @param  array  $parameters
587
     * @return mixed
588
     *
589
     * @throws \InvalidArgumentException
590
     * @throws \BadMethodCallException
591
     */
592 24
    public function __call($method, $parameters)
593
    {
594 24
        if ($this->isMagicRequestMethod($method, $requestMethod, $httpMethod)) {
595 8
            return $this->$requestMethod(...$this->getRequestParameters($httpMethod, $parameters));
596
        }
597
598 16
        if ($this->isMagicResponseMethod()) {
0 ignored issues
show
Bug introduced by
The call to ElfSundae\HttpClient::isMagicResponseMethod() has too few arguments starting with method. ( Ignorable by Annotation )

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

598
        if ($this->/** @scrutinizer ignore-call */ isMagicResponseMethod()) {

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
599
            return $this->getResponseData($method, $parameters);
600
        }
601
602
        if ($option = $this->getOptionNameForMethod($method)) {
603
            if (empty($parameters)) {
604
                throw new InvalidArgumentException("Method [$method] needs one argument.");
605
            }
606
607
            return $this->option($option, $parameters[0]);
608
        }
609
610
        throw new BadMethodCallException("Method [$method] does not exist.");
611
    }
612
}
613