Completed
Push — master ( 202605...018ad1 )
by Andrii
03:05
created

Connection::makeRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
ccs 0
cts 5
cp 0
rs 9.4286
cc 1
eloc 3
nc 1
nop 5
crap 2
1
<?php
2
3
/*
4
 * Tools to use API as ActiveRecord for Yii2
5
 *
6
 * @link      https://github.com/hiqdev/yii2-hiart
7
 * @package   yii2-hiart
8
 * @license   BSD-3-Clause
9
 * @copyright Copyright (c) 2015, HiQDev (http://hiqdev.com/)
10
 */
11
12
namespace hiqdev\hiart;
13
14
use Closure;
15
use Yii;
16
use yii\base\Component;
17
use yii\base\InvalidConfigException;
18
use yii\base\InvalidParamException;
19
use yii\helpers\Json;
20
21
/**
22
 * Connection class.
23
 *
24
 * Example configuration:
25
 * ```php
26
 * 'components' => [
27
 *     'hiart' => [
28
 *         'class' => 'hiqdev\hiart\Connection',
29
 *         'config' => [
30
 *             'api_url' => 'https://api.site.com/',
31
 *         ],
32
 *     ],
33
 * ],
34
 * ```
35
 */
36
class Connection extends Component
37
{
38
    const EVENT_AFTER_OPEN = 'afterOpen';
39
40
    public $config = [];
41
42
    public $connectionTimeout = null;
43
44
    public $dataTimeout = null;
45
46
    public static $curl = null;
47
48
    /**
49
     * Authorization config.
50
     *
51
     * @var array
52
     */
53
    protected $_auth;
54
55
    public function setAuth($auth)
56
    {
57
        $this->_auth = $auth;
58
    }
59
60
    public function getAuth()
61
    {
62
        if ($this->_auth instanceof Closure) {
63
            $this->_auth = call_user_func($this->_auth, $this);
0 ignored issues
show
Documentation Bug introduced by
It seems like call_user_func($this->_auth, $this) of type * is incompatible with the declared type array of property $_auth.

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...
64
        }
65
66
        return $this->_auth;
67
    }
68
69
    public function init()
70
    {
71
        if (!isset($this->config['api_url'])) {
72
            throw new InvalidConfigException('HiArt needs api_url configuration');
73
        }
74
    }
75
76
    public function getHandler()
77
    {
78
        if (!self::$curl) {
79
            self::$curl = static::$curl = curl_init();
80
        }
81
82
        return self::$curl;
83
    }
84
85
    /**
86
     * Closes the connection when this component is being serialized.
87
     * @return array
88
     */
89
    public function __sleep()
90
    {
91
        return array_keys(get_object_vars($this));
92
    }
93
94
    /**
95
     * Returns the name of the DB driver for the current [[dsn]].
96
     *
97
     * @return string name of the DB driver
98
     */
99
    public function getDriverName()
100
    {
101
        return 'hiresource';
102
    }
103
104
    /**
105
     * Creates a command for execution.
106
     *
107
     * @param array $config the configuration for the Command class
108
     *
109
     * @return Command the DB command
110
     */
111
    public function createCommand($config = [])
112
    {
113
        $config['db'] = $this;
114
        $command      = new Command($config);
115
116
        return $command;
117
    }
118
119
    /**
120
     * Creates new query builder instance.
121
     *
122
     * @return QueryBuilder
123
     */
124
    public function getQueryBuilder()
125
    {
126
        return new QueryBuilder($this);
127
    }
128
129
    /**
130
     * Performs GET HTTP request.
131
     * @param string $url     URL
132
     * @param array  $options URL options
133
     * @param string $body    request body
134
     * @param bool   $raw     if response body contains JSON and should be decoded
135
     * @throws HiArtException
136
     * @throws \yii\base\InvalidConfigException
137
     * @return mixed response
138
     */
139
    public function get($url, $options = [], $body = null, $raw = false)
0 ignored issues
show
Unused Code introduced by
The parameter $body is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
140
    {
141
        $result = $this->httpRequest('POST', $this->createUrl($url), http_build_query($options), $raw);
0 ignored issues
show
Documentation introduced by
$this->createUrl($url) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
142
143
        return $this->checkResponse($result, $url, $options);
144
    }
145
146
    /**
147
     * Performs HEAD HTTP request.
148
     * @param string $url     URL
149
     * @param array  $options URL options
150
     * @param string $body    request body
151
     * @throws HiArtException
152
     * @throws \yii\base\InvalidConfigException
153
     * @return mixed response
154
     */
155
    public function head($url, $options = [], $body = null)
0 ignored issues
show
Unused Code introduced by
The parameter $body is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
156
    {
157
        $result = $this->httpRequest('HEAD', $this->createUrl($url), http_build_query($options));
0 ignored issues
show
Documentation introduced by
$this->createUrl($url) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
158
159
        return $this->checkResponse($result, $url, $options);
160
    }
161
162
    /**
163
     * Performs POST HTTP request.
164
     * @param string $url     URL
165
     * @param array  $options URL options
166
     * @param string $body    request body
167
     * @param bool   $raw     if response body contains JSON and should be decoded
168
     * @throws HiArtException
169
     * @throws \yii\base\InvalidConfigException
170
     * @return mixed response
171
     */
172
    public function post($url, $options = [], $body = null, $raw = false)
0 ignored issues
show
Unused Code introduced by
The parameter $body is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
173
    {
174
        $result = $this->httpRequest('POST', $this->createUrl($url), http_build_query($options), $raw);
0 ignored issues
show
Documentation introduced by
$this->createUrl($url) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
175
176
        return $this->checkResponse($result, $url, $options);
177
    }
178
179
    /**
180
     * Performs PUT HTTP request.
181
     *
182
     * @param string $url     URL
183
     * @param array  $options URL options
184
     * @param string $body    request body
185
     * @param bool   $raw     if response body contains JSON and should be decoded
186
     *
187
     * @throws HiArtException
188
     * @throws \yii\base\InvalidConfigException
189
     *
190
     * @return mixed response
191
     */
192
    public function put($url, $options = [], $body = null, $raw = false)
0 ignored issues
show
Unused Code introduced by
The parameter $body is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
193
    {
194
        $result = $this->httpRequest('PUT', $this->createUrl($url), http_build_query($options), $raw);
0 ignored issues
show
Documentation introduced by
$this->createUrl($url) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
195
196
        return $this->checkResponse($result, $url, $options);
197
    }
198
199
    /**
200
     * Performs DELETE HTTP request.
201
     *
202
     * @param string $url     URL
203
     * @param array  $options URL options
204
     * @param string $body    request body
205
     * @param bool   $raw     if response body contains JSON and should be decoded
206
     *
207
     * @throws HiArtException
208
     * @throws \yii\base\InvalidConfigException
209
     *
210
     * @return mixed response
211
     */
212
    public function delete($url, $options = [], $body = null, $raw = false)
0 ignored issues
show
Unused Code introduced by
The parameter $body is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
213
    {
214
        $result = $this->httpRequest('DELETE', $this->createUrl($url), http_build_query($options), $raw);
0 ignored issues
show
Documentation introduced by
$this->createUrl($url) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
215
216
        return $this->checkResponse($result, $url, $options);
217
    }
218
219
    /**
220
     * @param $url
221
     * @param array $options
222
     * @return mixed
223
     */
224
    public function perform($url, $options = [])
225
    {
226
        $result = $this->httpRequest('POST', $this->createUrl($url), http_build_query($options));
0 ignored issues
show
Documentation introduced by
$this->createUrl($url) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
227
228
        return $this->checkResponse($result, $url, $options);
229
    }
230
231
    /**
232
     * Make request and check for error.
233
     * @param string $url     URL
234
     * @param array  $options URL options
235
     * @param string $body    request body
236
     * @param bool   $raw     if response body contains JSON and should be decoded
237
     * @throws HiArtException
238
     * @throws \yii\base\InvalidConfigException
239
     * @return mixed response
240
     */
241
    public function makeRequest($method, $url, $options = [], $body = null, $raw = false)
0 ignored issues
show
Unused Code introduced by
The parameter $body is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
242
    {
243
        $result = $this->httpRequest($method, $this->createUrl($url), http_build_query($options), $raw);
0 ignored issues
show
Documentation introduced by
$this->createUrl($url) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
244
245
        return $this->checkResponse($result, $url, $options);
246
    }
247
    /**
248
     * Creates URL.
249
     * @param mixed $path path
250
     * @param array $options URL options
251
     * @return array
252
     */
253
    private function createUrl($path, array $options = [])
254
    {
255
        $options = array_merge($this->getAuth(), $options);
256
        if (!is_string($path)) {
257
            $url = urldecode(reset($path));
258
            if (!empty($options)) {
259
                $url .= '?' . http_build_query($options);
260
            }
261
        } else {
262
            $url = $path;
263
            if (!empty($options)) {
264
                $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($options);
265
            }
266
        }
267
268
        return [$this->config['api_url'], $url];
269
    }
270
271
    /**
272
     * Performs HTTP request.
273
     * @param string $method method name
274
     * @param string $url URL
275
     * @param string $requestBody request body
276
     * @param bool $raw if response body contains JSON and should be decoded
277
     * @throws ErrorResponseException
278
     * @throws HiArtException
279
     * @return mixed if request failed
280
     */
281
    protected function httpRequest($method, $url, $requestBody = null, $raw = false)
282
    {
283
        $method = strtoupper($method);
284
        // response body and headers
285
        $headers = [];
286
        $body    = '';
287
        $options = [
288
            CURLOPT_URL       => $url,
289
            CURLOPT_USERAGENT => 'Yii Framework ' . Yii::getVersion() . ' (HiArt)',
290
            //CURLOPT_ENCODING        => 'UTF-8',
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
291
            # CURLOPT_USERAGENT       => 'curl/0.00 (php 5.x; U; en)',
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
292
            CURLOPT_RETURNTRANSFER => false,
293
            CURLOPT_HEADER         => false,
294
            CURLOPT_SSL_VERIFYPEER => false,
295
            CURLOPT_SSL_VERIFYHOST => 2,
296
            // http://www.php.net/manual/en/function.curl-setopt.php#82418
297
            CURLOPT_HTTPHEADER    => ['Expect:'],
298
            CURLOPT_WRITEFUNCTION => function ($curl, $data) use (&$body) {
299
                $body .= $data;
300
301
                return mb_strlen($data, '8bit');
302
            },
303
            CURLOPT_HEADERFUNCTION => function ($curl, $data) use (&$headers) {
304
                foreach (explode("\r\n", $data) as $row) {
305
                    if (($pos = strpos($row, ':')) !== false) {
306
                        $headers[strtolower(substr($row, 0, $pos))] = trim(substr($row, $pos + 1));
307
                    }
308
                }
309
310
                return mb_strlen($data, '8bit');
311
            },
312
            CURLOPT_CUSTOMREQUEST => $method,
313
        ];
314
        if ($this->connectionTimeout !== null) {
315
            $options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout;
316
        }
317
        if ($this->dataTimeout !== null) {
318
            $options[CURLOPT_TIMEOUT] = $this->dataTimeout;
319
        }
320
        if ($requestBody !== null) {
321
            $options[CURLOPT_POSTFIELDS] = $requestBody;
322
        }
323
        if ($method === 'HEAD') {
324
            $options[CURLOPT_NOBODY] = true;
325
            unset($options[CURLOPT_WRITEFUNCTION]);
326
        }
327
        if (is_array($url)) {
328
            list($host, $q) = $url;
329
            if (strncmp($host, 'inet[', 5) === 0) {
330
                $host = substr($host, 5, -1);
331
                if (($pos = strpos($host, '/')) !== false) {
332
                    $host = substr($host, $pos + 1);
333
                }
334
            }
335
            $profile = $method . ' ' . $q . '#' . $requestBody;
336
            if (preg_match('@^https?://@', $host)) {
337
                $url = $host . '/' . $q;
338
            } else {
339
                throw new HiArtException('Request failed: please specify the protocol (http, https) in reference to the API HiResource Core');
340
            }
341
        } else {
342
            $profile = false;
343
        }
344
        $options[CURLOPT_URL] = $url;
345
        Yii::trace("Sending request to node: $method $url\n$requestBody", __METHOD__);
346
        if ($profile !== false) {
347
            Yii::beginProfile($profile, __METHOD__);
348
        }
349
        $curl = $this->getHandler();
350
        curl_setopt_array($curl, $options);
351
        if (curl_exec($curl) === false) {
352
            throw new HiArtException('Request failed: ' . curl_errno($curl) . ' - ' . curl_error($curl), [
353
                'requestUrl'      => $url,
354
                'requestBody'     => $requestBody,
355
                'responseBody'    => $this->decodeErrorBody($body),
356
                'requestMethod'   => $method,
357
                'responseHeaders' => $headers,
358
            ]);
359
        }
360
361
        $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
362
        Yii::trace(curl_getinfo($curl));
363
        if ($profile !== false) {
364
            Yii::endProfile($profile, __METHOD__);
365
        }
366
        if ($responseCode >= 200 && $responseCode < 300) {
367
            if ($method === 'HEAD') {
368
                return true;
369
            } else {
370
                if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) {
371
                    throw new HiArtException("Incomplete data received: $len < {$headers['content-length']}", [
372
                        'requestMethod'   => $method,
373
                        'requestUrl'      => $url,
374
                        'requestBody'     => $requestBody,
375
                        'responseCode'    => $responseCode,
376
                        'responseHeaders' => $headers,
377
                        'responseBody'    => $this->decodeErrorBody($body),
378
                    ]);
379
                }
380
                if (isset($headers['content-type']) && !strncmp($headers['content-type'], 'application/json', 16)) {
381
                    return $raw ? $body : Json::decode($body);
382
                } else {
383
                    return $body;
384
                }
385
                throw new HiArtException('Unsupported data received from Hiresource: ' . $headers['content-type'], [
0 ignored issues
show
Unused Code introduced by
throw new \hiqdev\hiart\...Headers' => $headers)); does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
386
                    'requestUrl'      => $url,
387
                    'requestBody'     => $requestBody,
388
                    'responseBody'    => $this->decodeErrorBody($body),
389
                    'requestMethod'   => $method,
390
                    'responseCode'    => $responseCode,
391
                    'responseHeaders' => $headers,
392
                ]);
393
            }
394
        } elseif ($responseCode === 404) {
395
            return false;
396
        } else {
397
            throw new HiArtException("Request request failed with code $responseCode.", [
398
                'requestUrl'      => $url,
399
                'requestBody'     => $requestBody,
400
                'responseBody'    => $this->decodeErrorBody($body),
401
                'requestMethod'   => $method,
402
                'responseCode'    => $responseCode,
403
                'responseHeaders' => $headers,
404
            ]);
405
        }
406
    }
407
408
    /**
409
     * Try to decode error information if it is valid json, return it if not.
410
     * @param $body
411
     * @return mixed
412
     */
413
    protected function decodeErrorBody($body)
414
    {
415
        try {
416
            $decoded = Json::decode($body);
417
            if (isset($decoded['error'])) {
418
                $decoded['error'] = preg_replace('/\b\w+?Exception\[/',
419
                    "<span style=\"color: red;\">\\0</span>\n               ", $decoded['error']);
420
            }
421
422
            return $decoded;
423
        } catch (InvalidParamException $e) {
424
            return $body;
425
        }
426
    }
427
428
    /**
429
     * Callback to test if API response has error.
430
     */
431
    public $errorChecker;
432
433
    /**
434
     * Checks response with errorChecker callback and raises exception if error.
435
     * @param array  $response response data from API
436
     * @param string $url      request URL
437
     * @param array  $options  request data
438
     * @throws ErrorResponseException
439
     * @return array
440
     */
441
    protected function checkResponse($response, $url, $options)
442
    {
443
        $error = call_user_func($this->errorChecker, $response);
444
        if ($error) {
445
            throw new ErrorResponseException($error, [
446
                'requestUrl' => $url,
447
                'request'    => $options,
448
                'response'   => $response,
449
            ]);
450
        }
451
452
        return $response;
453
    }
454
}
455