Completed
Push — master ( f74bed...f040ce )
by Elf
01:43
created

HttpClient::getMagicResponseMethods()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 2
cts 2
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
crap 1
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
class HttpClient
17
{
18
    /**
19
     * The default request options.
20
     *
21
     * @var array
22
     */
23
    protected static $defaultOptions = [
24
        'connect_timeout' => 5,
25
        'timeout' => 30,
26
    ];
27
28
    /**
29
     * The Guzzle client.
30
     *
31
     * @var \GuzzleHttp\Client
32
     */
33
    protected $client;
34
35
    /**
36
     * The request options.
37
     *
38
     * @var array
39
     */
40
    protected $options = [];
41
42
    /**
43
     * The Guzzle response.
44
     *
45
     * @var \GuzzleHttp\Psr7\Response
46
     */
47
    protected $response;
48
49
    /**
50
     * Indicate whether to catch Guzzle exceptions.
51
     *
52
     * @var bool
53
     */
54
    protected $catchExceptions = true;
55
56
    /**
57
     * Get the default request options.
58
     *
59
     * @return array
60
     */
61 26
    public static function defaultOptions()
62
    {
63 26
        return static::$defaultOptions;
64
    }
65
66
    /**
67
     * Set the default request options.
68
     *
69
     * @param  array  $options
70
     * @return void
71
     */
72 27
    public static function setDefaultOptions(array $options)
73
    {
74 27
        static::$defaultOptions = $options;
75 27
    }
76
77
    /**
78
     * Create a http client instance.
79
     *
80
     * @param  array|string|\Psr\Http\Message\UriInterface  $options  base URI or any request options
81
     *
82
     * @throws \InvalidArgumentException
83
     */
84 26
    public function __construct($options = [])
85
    {
86 26
        if (is_string($options) || $options instanceof UriInterface) {
87 1
            $options = ['base_uri' => $options];
88 25
        } elseif (! is_array($options)) {
89 1
            throw new InvalidArgumentException('Options must be a string, UriInterface, or an array');
90
        }
91
92 25
        $this->client = new Client(
93 25
            array_replace_recursive(static::defaultOptions(), $options)
94
        );
95
96 25
        $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...
97 25
    }
98
99
    /**
100
     * Get the Guzzle client instance.
101
     *
102
     * @return \GuzzleHttp\Client
103
     */
104 4
    public function getClient()
105
    {
106 4
        return $this->client;
107
    }
108
109
    /**
110
     * Get whether to catch Guzzle exceptions or not.
111
     *
112
     * @return bool
113
     */
114 1
    public function areExceptionsCaught()
115
    {
116 1
        return $this->catchExceptions;
117
    }
118
119
    /**
120
     * Set whether to catch Guzzle exceptions or not.
121
     *
122
     * @param  bool  $catch
123
     * @return $this
124
     */
125 2
    public function catchExceptions($catch)
126
    {
127 2
        $this->catchExceptions = (bool) $catch;
128
129 2
        return $this;
130
    }
131
132
    /**
133
     * Get the request options using "dot" notation.
134
     *
135
     * @param  string|null  $key
136
     * @return mixed
137
     */
138 11
    public function getOption($key = null)
139
    {
140 11
        return Arr::get($this->options, $key);
141
    }
142
143
    /**
144
     * Set the request options using "dot" notation.
145
     *
146
     * @param  string|array  $key
147
     * @param  mixed  $value
148
     * @return $this
149
     */
150 8
    public function option($key, $value = null)
151
    {
152 8
        $keys = is_array($key) ? $key : [$key => $value];
153
154 8
        foreach ($keys as $key => $value) {
155 8
            Arr::set($this->options, $key, $value);
156
        }
157
158 8
        return $this;
159
    }
160
161
    /**
162
     * Merge the given options to the request options.
163
     *
164
     * @param  array  ...$options
165
     * @return $this
166
     */
167 1
    public function mergeOption(array ...$options)
168
    {
169 1
        $this->options = array_replace_recursive($this->options, ...$options);
170
171 1
        return $this;
172
    }
173
174
    /**
175
     * Remove one or many request options using "dot" notation.
176
     *
177
     * @param  array|string  $keys
178
     * @return $this
179
     */
180 2
    public function removeOption($keys)
181
    {
182 2
        Arr::forget($this->options, is_array($keys) ? $keys : func_get_args());
183
184 2
        return $this;
185
    }
186
187
    /**
188
     * Set a request header.
189
     *
190
     * @param  string  $name
191
     * @param  mixed  $value
192
     * @return $this
193
     */
194 5
    public function header($name, $value)
195
    {
196 5
        return $this->option('headers.'.$name, $value);
197
    }
198
199
    /**
200
     * Set the request content type.
201
     *
202
     * @param  string  $type
203
     * @return $this
204
     */
205 1
    public function contentType($type)
206
    {
207 1
        return $this->header('Content-Type', $type);
208
    }
209
210
    /**
211
     * Set the request accept type.
212
     *
213
     * @param  string  $type
214
     * @return $this
215
     */
216 3
    public function accept($type)
217
    {
218 3
        return $this->header('Accept', $type);
219
    }
220
221
    /**
222
     * Set the request accept type to "application/json".
223
     *
224
     * @return $this
225
     */
226 1
    public function acceptJson()
227
    {
228 1
        return $this->accept('application/json');
229
    }
230
231
    /**
232
     * Specify where the body of the response will be saved.
233
     * Set the "sink" option.
234
     *
235
     * @param  mixed  $dest
236
     * @return $this
237
     */
238 1
    public function saveTo($dest)
239
    {
240 1
        return $this->removeOption('save_to')->option('sink', $dest);
241
    }
242
243
    /**
244
     * Get the Guzzle response instance.
245
     *
246
     * @return \GuzzleHttp\Psr7\Response|null
247
     */
248 3
    public function getResponse()
249
    {
250 3
        return $this->response;
251
    }
252
253
    /**
254
     * Get data from the response.
255
     *
256
     * @param  string|\Closure  $callback
257
     * @param  array  $parameters
258
     * @param  mixed  $default
259
     * @return mixed
260
     */
261 5
    protected function getResponseData($callback, array $parameters = [], $default = null)
262
    {
263 5
        if ($this->response) {
264 5
            return $callback instanceof Closure
265
                ? $callback($this->response, ...$parameters)
266 5
                : $this->response->$callback(...$parameters);
267
        }
268
269
        return $default;
270
    }
271
272
    /**
273
     * Get the response content.
274
     *
275
     * @return string
276
     */
277 4
    public function getContent()
278
    {
279 4
        return (string) $this->getBody();
0 ignored issues
show
Documentation Bug introduced by
The method getBody does not exist on object<ElfSundae\HttpClient>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
280
    }
281
282
    /**
283
     * Get the JSON-decoded response content.
284
     *
285
     * @param  bool  $assoc
286
     * @return mixed
287
     */
288 2
    public function getJsonContent($assoc = true)
289
    {
290 2
        return json_decode($this->getContent(), $assoc);
291
    }
292
293
    /**
294
     * Make request to a URI.
295
     *
296
     * @param  string  $uri
297
     * @param  string  $method
298
     * @param  array  $options
299
     * @return $this
300
     */
301 10
    public function request($uri = '', $method = 'GET', array $options = [])
302
    {
303 10
        $this->response = null;
304
305 10
        $method = strtoupper($method);
306 10
        $options = array_replace_recursive($this->options, $options);
307
308
        try {
309 10
            $this->response = $this->client->request($method, $uri, $options);
310 1
        } catch (Exception $e) {
311 1
            if (! $this->catchExceptions) {
312 1
                throw $e;
313
            }
314
        }
315
316 10
        return $this;
317
    }
318
319
    /**
320
     * Make request to a URI, expecting JSON content.
321
     *
322
     * @param  string  $uri
323
     * @param  string  $method
324
     * @param  array  $options
325
     * @return $this
326
     */
327 4
    public function requestJson($uri = '', $method = 'GET', array $options = [])
328
    {
329 4
        $options = $this->addAcceptableJsonType(
330 4
            array_replace_recursive($this->options, $options)
331
        );
332
333 4
        return $this->request($uri, $method, $options);
334
    }
335
336
    /**
337
     * Add JSON type to the "Accept" header for the request options.
338
     *
339
     * @param  array  $options
340
     * @return array
341
     */
342 4
    protected function addAcceptableJsonType(array $options)
343
    {
344 4
        $accept = Arr::get($options, 'headers.Accept', '');
345
346 4
        if (! Str::contains($accept, ['/json', '+json'])) {
347 4
            $accept = rtrim('application/json,'.$accept, ',');
348 4
            Arr::set($options, 'headers.Accept', $accept);
349
        }
350
351 4
        return $options;
352
    }
353
354
    /**
355
     * Request the URI and return the response content.
356
     *
357
     * @param  string  $uri
358
     * @param  string  $method
359
     * @param  array  $options
360
     * @return string
361
     */
362 1
    public function fetchContent($uri = '', $method = 'GET', array $options = [])
363
    {
364 1
        return $this->request($uri, $method, $options)->getContent();
365
    }
366
367
    /**
368
     * Request the URI and return the JSON-decoded response content.
369
     *
370
     * @param  string  $uri
371
     * @param  string  $method
372
     * @param  array  $options
373
     * @return mixed
374
     */
375 1
    public function fetchJson($uri = '', $method = 'GET', array $options = [])
376
    {
377 1
        return $this->requestJson($uri, $method, $options)->getJsonContent();
378
    }
379
380
    /**
381
     * Get all allowed magic request methods.
382
     *
383
     * @return array
384
     */
385 9
    protected function getMagicRequestMethods()
386
    {
387
        return [
388 9
            'get', 'head', 'put', 'post', 'patch', 'delete', 'options',
389
        ];
390
    }
391
392
    /**
393
     * Get all allowed magic response methods.
394
     *
395
     * @return array
396
     */
397 7
    protected function getMagicResponseMethods()
398
    {
399
        return [
400 7
            'getStatusCode', 'getReasonPhrase', 'getProtocolVersion',
401
            'getHeaders', 'hasHeader', 'getHeader', 'getHeaderLine', 'getBody',
402
        ];
403
    }
404
405
    /**
406
     * Get all allowed magic option methods.
407
     *
408
     * @return array
409
     */
410 2
    protected function getMagicOptionMethods()
411
    {
412 2
        static $optionMethods = null;
413
414 2
        if (is_null($optionMethods)) {
415 1
            $reflector = new ReflectionClass(RequestOptions::class);
416 1
            $optionMethods = array_map(
417 1
                [Str::class, 'camel'],
418 1
                array_values($reflector->getConstants())
419
            );
420
        }
421
422 2
        return $optionMethods;
423
    }
424
425
    /**
426
     * Determine if the given method is a magic request method.
427
     *
428
     * @param  string  $method
429
     * @param  string  &$requestMethod
430
     * @param  string  &$httpMethod
431
     * @return bool
432
     */
433 9
    protected function isMagicRequest($method, &$requestMethod, &$httpMethod)
434
    {
435 9
        if (strlen($method) > 4 && $pos = strrpos($method, 'Json', -4)) {
436 2
            $httpMethod = substr($method, 0, $pos);
437 2
            $requestMethod = 'requestJson';
438
        } else {
439 9
            $httpMethod = $method;
440 9
            $requestMethod = 'request';
441
        }
442
443 9
        if (in_array($httpMethod, $this->getMagicRequestMethods())) {
444 2
            return true;
445
        }
446
447 7
        $httpMethod = $requestMethod = null;
448
449 7
        return false;
450
    }
451
452
    /**
453
     * Get parameters for $this->request() from the magic request methods.
454
     *
455
     * @param  string  $httpMethod
456
     * @param  array  $parameters
457
     * @return array
458
     */
459 2
    protected function getRequestParameters($httpMethod, array $parameters)
460
    {
461 2
        if (empty($parameters)) {
462 1
            $parameters = ['', $httpMethod];
463
        } else {
464 2
            array_splice($parameters, 1, 0, $httpMethod);
465
        }
466
467 2
        return $parameters;
468
    }
469
470
    /**
471
     * Handle magic method to send request, get response data, or set
472
     * request options.
473
     *
474
     * @param  string  $method
475
     * @param  array  $parameters
476
     * @return mixed
477
     *
478
     * @throws \InvalidArgumentException
479
     * @throws \BadMethodCallException
480
     */
481 9
    public function __call($method, $parameters)
482
    {
483 9
        if ($this->isMagicRequest($method, $request, $httpMethod)) {
484 2
            return $this->{$request}(
485 2
                ...$this->getRequestParameters($httpMethod, $parameters)
486
            );
487
        }
488
489 7
        if (in_array($method, $this->getMagicResponseMethods())) {
490 5
            return $this->getResponseData($method, $parameters);
491
        }
492
493 2
        if (in_array($method, $this->getMagicOptionMethods())) {
494 1
            if (empty($parameters)) {
495 1
                throw new InvalidArgumentException("Method [$method] needs one argument.");
496
            }
497
498 1
            return $this->option(Str::snake($method), $parameters[0]);
499
        }
500
501 1
        throw new BadMethodCallException("Method [$method] does not exist.");
502
    }
503
}
504