Passed
Pull Request — master (#390)
by Arman
03:28
created

HttpClient::ensureSingleRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 4
rs 10
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 2.9.9
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
    /**
38
     * Available methods
39
     */
40
    const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
41
42
    /**
43
     * Response headers section
44
     */
45
    const RESPONSE_HEADERS = 'headers';
46
47
    /**
48
     * Response cookies section
49
     */
50
    const RESPONSE_COOKIES = 'cookies';
51
52
    /**
53
     * Response body section
54
     */
55
    const RESPONSE_BODY = 'body';
56
57
    /**
58
     * @var MultiCurl|Curl
59
     */
60
    private $client = null;
61
62
    /**
63
     * @var string
64
     */
65
    private $method = 'GET';
66
67
    /**
68
     * @var mixed|null
69
     */
70
    private $data = null;
71
72
    /**
73
     * @var array
74
     */
75
    private array $requestHeaders = [];
76
77
    /**
78
     * @var array
79
     */
80
    private $response = [];
81
82
    /**
83
     * @var array
84
     */
85
    private $errors = [];
86
87
    /**
88
     * Creates request
89
     * @param string $url
90
     * @return HttpClient
91
     */
92
    public function createRequest(string $url): HttpClient
93
    {
94
        $this->client = new Curl();
95
        $this->client->setUrl($url);
96
        return $this;
97
    }
98
99
    /**
100
     * Creates multi request
101
     * @return HttpClient
102
     */
103
    public function createMultiRequest(): HttpClient
104
    {
105
        $this->client = new MultiCurl();
106
107
        $this->client->complete(function (Curl $instance) {
108
            $this->handleResponse($instance);
109
        });
110
111
        return $this;
112
    }
113
114
    /**
115
     * Creates async multi request
116
     * @param callable $success
117
     * @param callable $error
118
     * @return HttpClient
119
     */
120
    public function createAsyncMultiRequest(callable $success, callable $error): HttpClient
121
    {
122
        $this->client = new MultiCurl();
123
124
        $this->client->success($success);
125
        $this->client->error($error);
126
127
        return $this;
128
    }
129
130
    /**
131
     * Sets http method
132
     * @param string $method
133
     * @return $this
134
     * @throws BaseException
135
     */
136
    public function setMethod(string $method): HttpClient
137
    {
138
        if (!in_array($method, self::METHODS)) {
139
            throw HttpClientException::requestMethodNotAvailable($method);
140
        }
141
142
        $this->method = $method;
143
        return $this;
144
    }
145
146
    /**
147
     * Gets the current http method
148
     * @return string
149
     */
150
    public function getMethod(): string
151
    {
152
        return $this->method;
153
    }
154
155
    /**
156
     * Sets data
157
     * @param mixed $data
158
     * @return HttpClient
159
     */
160
    public function setData($data): HttpClient
161
    {
162
        $this->data = $data;
163
        return $this;
164
    }
165
166
    /**
167
     * Gets the data
168
     * @return mixed|null
169
     */
170
    public function getData()
171
    {
172
        return $this->data;
173
    }
174
175
    /**
176
     * Checks if the request is multi cURL
177
     * @return bool
178
     */
179
    public function isMultiRequest(): bool
180
    {
181
        return $this->client instanceof MultiCurl;
182
    }
183
184
    /**
185
     * Starts the request
186
     * @throws ErrorException
187
     * @throws HttpClientException
188
     */
189
    public function start(): HttpClient
190
    {
191
        if (!$this->client) {
192
            throw HttpClientException::requestNotCreated();
193
        }
194
195
        if ($this->isMultiRequest()) {
196
            $this->client->start();
197
        } else {
198
            $this->startSingleRequest();
199
        }
200
201
        return $this;
202
    }
203
204
    /**
205
     * Gets single or all request headers
206
     * @param string|null $header
207
     * @return mixed|null
208
     * @throws BaseException
209
     */
210
    public function getRequestHeaders(string $header = null)
211
    {
212
        $this->ensureSingleRequest();
213
214
        if ($header !== null) {
215
            return $this->requestHeaders[$header] ?? null;
216
        }
217
218
        return $this->requestHeaders;
219
    }
220
221
    /**
222
     * Gets the response headers
223
     * @param string|null $header
224
     * @return mixed|null
225
     * @throws BaseException
226
     */
227
    public function getResponseHeaders(string $header = null)
228
    {
229
        $this->ensureSingleRequest();
230
231
        $responseHeaders = $this->getResponse()[self::RESPONSE_HEADERS];
232
233
        if ($header) {
234
            return $responseHeaders[$header] ?? null;
235
        }
236
237
        return $responseHeaders;
238
    }
239
240
    /**
241
     * Gets the response cookies
242
     * @param string|null $cookie
243
     * @return mixed|null
244
     * @throws BaseException
245
     */
246
    public function getResponseCookies(string $cookie = null)
247
    {
248
        $this->ensureSingleRequest();
249
250
        $responseCookies = $this->getResponse()[self::RESPONSE_COOKIES];
251
252
        if ($cookie) {
253
            return $responseCookies[$cookie] ?? null;
254
        }
255
256
        return $responseCookies;
257
    }
258
259
    /**
260
     * Gets the response body
261
     * @return mixed|null
262
     * @throws BaseException
263
     */
264
    public function getResponseBody()
265
    {
266
        $this->ensureSingleRequest();
267
268
        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

268
        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...
269
    }
270
271
    /**
272
     * Gets the entire response
273
     * @return array
274
     */
275
    public function getResponse(): array
276
    {
277
        return $this->isMultiRequest() ? $this->response : ($this->response[$this->client->getId()] ?? []);
278
    }
279
280
    /**
281
     * Returns the errors
282
     * @return array
283
     */
284
    public function getErrors(): array
285
    {
286
        return $this->isMultiRequest() ? $this->errors : ($this->errors[$this->client->getId()] ?? []);
287
    }
288
289
    /**
290
     * Gets the curl info
291
     * @param int|null $option
292
     * @return mixed
293
     * @throws BaseException
294
     */
295
    public function info(int $option = null)
296
    {
297
        $this->ensureSingleRequest();
298
299
        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

299
        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...
300
    }
301
302
    /**
303
     * Gets the current url being executed
304
     * @return string|null
305
     * @throws BaseException
306
     */
307
    public function url(): ?string
308
    {
309
        $this->ensureSingleRequest();
310
311
        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

311
        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...
312
    }
313
314
    /**
315
     * @param string $method
316
     * @param array $arguments
317
     * @return $this
318
     * @throws BaseException
319
     * @throws HttpClientException
320
     */
321
    public function __call(string $method, array $arguments): HttpClient
322
    {
323
        if (is_null($this->client)) {
324
            throw HttpClientException::requestNotCreated();
325
        }
326
327
        if (!method_exists($this->client, $method)) {
328
            throw HttpClientException::methodNotSupported($method, get_class($this->client));
329
        }
330
331
        $this->interceptCall($method, $arguments);
332
333
        $this->client->$method(...$arguments);
334
335
        return $this;
336
    }
337
338
    /**
339
     * @return void
340
     * @throws ErrorException
341
     */
342
    private function startSingleRequest(): void
343
    {
344
        $this->client->setOpt(CURLOPT_CUSTOMREQUEST, $this->method);
345
346
        if ($this->data) {
347
            $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

347
            $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...
348
        }
349
350
        $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

350
        $this->client->/** @scrutinizer ignore-call */ 
351
                       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...
351
        $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

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

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