HttpClient   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 387
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 95
dl 0
loc 387
rs 8.72
c 2
b 0
f 0
wmc 46

23 Methods

Rating   Name   Duplication   Size   Complexity  
A url() 0 5 1
A getRequestHeaders() 0 9 2
A startSingleRequest() 0 10 2
A interceptCall() 0 14 6
A createAsyncMultiRequest() 0 8 2
A formatHeaders() 0 9 2
A getResponseBody() 0 5 1
A getResponseCookies() 0 11 2
A ensureSingleRequest() 0 4 2
A setData() 0 4 1
A info() 0 5 2
A createRequest() 0 5 2
A isMultiRequest() 0 3 1
A getErrors() 0 3 2
A createMultiRequest() 0 9 2
A getMethod() 0 3 1
A getResponse() 0 3 2
A getResponseHeaders() 0 11 2
A handleResponse() 0 13 2
A setMethod() 0 8 2
A start() 0 13 3
A getData() 0 3 1
A __call() 0 15 3

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.

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
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 3.0.0
13
 */
14
15
namespace Quantum\Libraries\HttpClient;
16
17
use Quantum\Libraries\HttpClient\Exceptions\HttpClientException;
18
use Quantum\App\Exceptions\BaseException;
19
use Curl\CaseInsensitiveArray;
20
use Curl\MultiCurl;
21
use ErrorException;
22
use Curl\Curl;
23
24
/**
25
 * HttpClient Class
26
 * @package Quantum\Libraries\HttpClient
27
 * @uses php-curl-class/php-curl-class
28
 * @method object addGet(string $url, array $data = [])
29
 * @method object addPost(string $url, string $data = '', bool $follow_303_with_post = false)
30
 * @method setHeader($key, $value)
31
 * @method setHeaders($headers)
32
 * @method setOpt($option, $value)
33
 */
34
class HttpClient
35
{
36
    /**
37
     * Available methods
38
     */
39
    public const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
40
41
    /**
42
     * Response headers section
43
     */
44
    public const RESPONSE_HEADERS = 'headers';
45
46
    /**
47
     * Response cookies section
48
     */
49
    public const RESPONSE_COOKIES = 'cookies';
50
51
    /**
52
     * Response body section
53
     */
54
    public const RESPONSE_BODY = 'body';
55
56
    /**
57
     * @var MultiCurl|Curl
58
     */
59
    private $client = null;
60
61
    /**
62
     * @var string
63
     */
64
    private $method = 'GET';
65
66
    /**
67
     * @var mixed|null
68
     */
69
    private $data = null;
70
71
    /**
72
     * @var array
73
     */
74
    private array $requestHeaders = [];
75
76
    /**
77
     * @var array
78
     */
79
    private $response = [];
80
81
    /**
82
     * @var array
83
     */
84
    private $errors = [];
85
86
    /**
87
     * Creates request
88
     * @param string $url
89
     * @param Curl|null $client
90
     * @return HttpClient
91
     */
92
    public function createRequest(string $url, ?Curl $client = null): HttpClient
93
    {
94
        $this->client = $client ?: new Curl();
95
        $this->client->setUrl($url);
96
        return $this;
97
    }
98
99
    /**
100
     * Creates multi request
101
     * @param MultiCurl|null $client
102
     * @return HttpClient
103
     */
104
    public function createMultiRequest(?MultiCurl $client = null): HttpClient
105
    {
106
        $this->client = $client ?: new MultiCurl();
107
108
        $this->client->complete(function (Curl $instance) {
109
            $this->handleResponse($instance);
110
        });
111
112
        return $this;
113
    }
114
115
    /**
116
     * Creates async multi request
117
     * @param callable $success
118
     * @param callable $error
119
     * @param MultiCurl|null $client
120
     * @return HttpClient
121
     */
122
    public function createAsyncMultiRequest(callable $success, callable $error, ?MultiCurl $client = null): HttpClient
123
    {
124
        $this->client = $client ?: new MultiCurl();
125
126
        $this->client->success($success);
127
        $this->client->error($error);
128
129
        return $this;
130
    }
131
132
    /**
133
     * Sets http method
134
     * @param string $method
135
     * @return $this
136
     * @throws BaseException
137
     */
138
    public function setMethod(string $method): HttpClient
139
    {
140
        if (!in_array($method, self::METHODS)) {
141
            throw HttpClientException::requestMethodNotAvailable($method);
142
        }
143
144
        $this->method = $method;
145
        return $this;
146
    }
147
148
    /**
149
     * Gets the current http method
150
     * @return string
151
     */
152
    public function getMethod(): string
153
    {
154
        return $this->method;
155
    }
156
157
    /**
158
     * Sets data
159
     * @param mixed $data
160
     * @return HttpClient
161
     */
162
    public function setData($data): HttpClient
163
    {
164
        $this->data = $data;
165
        return $this;
166
    }
167
168
    /**
169
     * Gets the data
170
     * @return mixed|null
171
     */
172
    public function getData()
173
    {
174
        return $this->data;
175
    }
176
177
    /**
178
     * Checks if the request is multi cURL
179
     * @return bool
180
     */
181
    public function isMultiRequest(): bool
182
    {
183
        return $this->client instanceof MultiCurl;
184
    }
185
186
    /**
187
     * Starts the request
188
     * @throws ErrorException
189
     * @throws HttpClientException
190
     */
191
    public function start(): HttpClient
192
    {
193
        if (!$this->client) {
194
            throw HttpClientException::requestNotCreated();
195
        }
196
197
        if ($this->isMultiRequest()) {
198
            $this->client->start();
199
        } else {
200
            $this->startSingleRequest();
201
        }
202
203
        return $this;
204
    }
205
206
    /**
207
     * Gets single or all request headers
208
     * @param string|null $header
209
     * @return mixed|null
210
     * @throws BaseException
211
     */
212
    public function getRequestHeaders(?string $header = null)
213
    {
214
        $this->ensureSingleRequest();
215
216
        if ($header !== null) {
217
            return $this->requestHeaders[$header] ?? null;
218
        }
219
220
        return $this->requestHeaders;
221
    }
222
223
    /**
224
     * Gets the response headers
225
     * @param string|null $header
226
     * @return mixed|null
227
     * @throws BaseException
228
     */
229
    public function getResponseHeaders(?string $header = null)
230
    {
231
        $this->ensureSingleRequest();
232
233
        $responseHeaders = $this->getResponse()[self::RESPONSE_HEADERS];
234
235
        if ($header) {
236
            return $responseHeaders[$header] ?? null;
237
        }
238
239
        return $responseHeaders;
240
    }
241
242
    /**
243
     * Gets the response cookies
244
     * @param string|null $cookie
245
     * @return mixed|null
246
     * @throws BaseException
247
     */
248
    public function getResponseCookies(?string $cookie = null)
249
    {
250
        $this->ensureSingleRequest();
251
252
        $responseCookies = $this->getResponse()[self::RESPONSE_COOKIES];
253
254
        if ($cookie) {
255
            return $responseCookies[$cookie] ?? null;
256
        }
257
258
        return $responseCookies;
259
    }
260
261
    /**
262
     * Gets the response body
263
     * @return mixed|null
264
     * @throws BaseException
265
     */
266
    public function getResponseBody()
267
    {
268
        $this->ensureSingleRequest();
269
270
        return $this->response[$this->client->getId()][self::RESPONSE_BODY] ?? null;
0 ignored issues
show
Bug introduced by
The method getId() does not exist on Curl\MultiCurl. ( Ignorable by Annotation )

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

270
        return $this->response[$this->client->/** @scrutinizer ignore-call */ getId()][self::RESPONSE_BODY] ?? null;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
271
    }
272
273
    /**
274
     * Gets the entire response
275
     * @return array
276
     */
277
    public function getResponse(): array
278
    {
279
        return $this->isMultiRequest() ? $this->response : ($this->response[$this->client->getId()] ?? []);
280
    }
281
282
    /**
283
     * Returns the errors
284
     * @return array
285
     */
286
    public function getErrors(): array
287
    {
288
        return $this->isMultiRequest() ? $this->errors : ($this->errors[$this->client->getId()] ?? []);
289
    }
290
291
    /**
292
     * Gets the curl info
293
     * @param int|null $option
294
     * @return mixed
295
     * @throws BaseException
296
     */
297
    public function info(?int $option = null)
298
    {
299
        $this->ensureSingleRequest();
300
301
        return $option ? $this->client->getInfo($option) : $this->client->getInfo();
0 ignored issues
show
Bug introduced by
The method getInfo() does not exist on Curl\MultiCurl. ( Ignorable by Annotation )

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

301
        return $option ? $this->client->/** @scrutinizer ignore-call */ getInfo($option) : $this->client->getInfo();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
302
    }
303
304
    /**
305
     * Gets the current url being executed
306
     * @return string|null
307
     * @throws BaseException
308
     */
309
    public function url(): ?string
310
    {
311
        $this->ensureSingleRequest();
312
313
        return $this->client->getUrl();
0 ignored issues
show
Bug introduced by
The method getUrl() does not exist on Curl\MultiCurl. ( Ignorable by Annotation )

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

313
        return $this->client->/** @scrutinizer ignore-call */ getUrl();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
314
    }
315
316
    /**
317
     * @param string $method
318
     * @param array $arguments
319
     * @return $this
320
     * @throws BaseException
321
     * @throws HttpClientException
322
     */
323
    public function __call(string $method, array $arguments): HttpClient
324
    {
325
        if (is_null($this->client)) {
326
            throw HttpClientException::requestNotCreated();
327
        }
328
329
        if (!method_exists($this->client, $method)) {
330
            throw HttpClientException::methodNotSupported($method, get_class($this->client));
331
        }
332
333
        $this->interceptCall($method, $arguments);
334
335
        $this->client->$method(...$arguments);
336
337
        return $this;
338
    }
339
340
    /**
341
     * @return void
342
     * @throws ErrorException
343
     */
344
    private function startSingleRequest(): void
345
    {
346
        $this->client->setOpt(CURLOPT_CUSTOMREQUEST, $this->method);
347
348
        if ($this->data) {
349
            $this->client->setOpt(CURLOPT_POSTFIELDS, $this->client->buildPostData($this->data));
0 ignored issues
show
Bug introduced by
The method buildPostData() does not exist on Curl\MultiCurl. ( Ignorable by Annotation )

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

349
            $this->client->setOpt(CURLOPT_POSTFIELDS, $this->client->/** @scrutinizer ignore-call */ buildPostData($this->data));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
350
        }
351
352
        $this->client->exec();
0 ignored issues
show
Bug introduced by
The method exec() does not exist on Curl\MultiCurl. ( Ignorable by Annotation )

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

352
        $this->client->/** @scrutinizer ignore-call */ 
353
                       exec();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
353
        $this->handleResponse($this->client);
0 ignored issues
show
Bug introduced by
It seems like $this->client can also be of type Curl\MultiCurl; however, parameter $instance of Quantum\Libraries\HttpCl...lient::handleResponse() does only seem to accept Curl\Curl, 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

353
        $this->handleResponse(/** @scrutinizer ignore-type */ $this->client);
Loading history...
354
    }
355
356
    /**
357
     * Handles the response
358
     * @param Curl $instance
359
     */
360
    private function handleResponse(Curl $instance)
361
    {
362
        if ($instance->isError()) {
363
            $this->errors[$instance->getId()] = [
364
                'code' => $instance->getErrorCode(),
365
                'message' => $instance->getErrorMessage(),
366
            ];
367
        }
368
369
        $this->response[$instance->getId()] = [
370
            self::RESPONSE_HEADERS => $this->formatHeaders($instance->getResponseHeaders()),
371
            self::RESPONSE_COOKIES => $instance->getResponseCookies(),
372
            self::RESPONSE_BODY => $instance->getResponse(),
373
        ];
374
    }
375
376
    /**
377
     * @param CaseInsensitiveArray $headers
378
     * @return array
379
     */
380
    private function formatHeaders(CaseInsensitiveArray $headers): array
381
    {
382
        $formatted = [];
383
384
        foreach ($headers as $key => $value) {
385
            $formatted[strtolower($key)] = $value;
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type null; however, parameter $string of strtolower() 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

385
            $formatted[strtolower(/** @scrutinizer ignore-type */ $key)] = $value;
Loading history...
386
        }
387
388
        return $formatted;
389
    }
390
391
    /**
392
     * @return void
393
     * @throws BaseException
394
     */
395
    private function ensureSingleRequest(): void
396
    {
397
        if ($this->isMultiRequest()) {
398
            throw HttpClientException::methodNotSupported(__METHOD__, MultiCurl::class);
399
        }
400
    }
401
402
    /**
403
     * @param string $method
404
     * @param array $arguments
405
     * @return void
406
     */
407
    private function interceptCall(string $method, array $arguments): void
408
    {
409
        switch ($method) {
410
            case 'setHeaders':
411
                if (isset($arguments[0]) && is_array($arguments[0])) {
412
                    $this->requestHeaders = array_change_key_case($arguments[0], CASE_LOWER);
413
                }
414
                break;
415
416
            case 'setHeader':
417
                if (isset($arguments[0], $arguments[1])) {
418
                    $this->requestHeaders[strtolower($arguments[0])] = $arguments[1];
419
                }
420
                break;
421
        }
422
    }
423
}
424