Completed
Push — master ( db1a24...cb1f5b )
by Andrii
17:43
created

Connection::init()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 6
ccs 0
cts 5
cp 0
rs 9.4286
cc 2
eloc 3
nc 2
nop 0
crap 6
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
 *             'base_uri' => 'https://api.site.com/',
31
 *         ],
32
 *     ],
33
 * ],
34
 * ```
35
 */
36
class Connection extends Component
37
{
38
    const EVENT_AFTER_OPEN = 'afterOpen';
39
40
    /**
41
     * @var array Config
42
     */
43
    public $config = [];
44
45
    public $connectionTimeout = null;
46
47
    public $dataTimeout = null;
48
49
    public static $curl = null;
50
51
    protected static $guzzle = null;
52
53
    /**
54
     * Authorization config.
55
     *
56
     * @var array
57
     */
58
    protected $_auth;
59
60
    public function setAuth($auth)
61
    {
62
        $this->_auth = $auth;
63
    }
64
65
    public function getAuth()
66
    {
67
        if ($this->_auth instanceof Closure) {
68
            $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...
69
        }
70
71
        return $this->_auth;
72
    }
73
74
    public function init()
75
    {
76
        if (!isset($this->config['api_url'])) {
77
            throw new InvalidConfigException('HiArt needs api_url configuration');
78
        }
79
    }
80
81
    public function getHandler()
82
    {
83
        if (!self::$curl) {
84
            self::$curl = static::$curl = curl_init();
85
        }
86
87
        return self::$curl;
88
    }
89
90
    /**
91
     * Closes the connection when this component is being serialized.
92
     * @return array
93
     */
94
    public function __sleep()
95
    {
96
        return array_keys(get_object_vars($this));
97
    }
98
99
    /**
100
     * Returns the name of the DB driver for the current [[dsn]].
101
     *
102
     * @return string name of the DB driver
103
     */
104
    public function getDriverName()
105
    {
106
        return 'hiresource';
107
    }
108
109
    /**
110
     * Creates a command for execution.
111
     *
112
     * @param array $config the configuration for the Command class
113
     *
114
     * @return Command the DB command
115
     */
116
    public function createCommand($config = [])
117
    {
118
        $config['db'] = $this;
119
        $command      = new Command($config);
120
121
        return $command;
122
    }
123
124
    /**
125
     * Creates new query builder instance.
126
     *
127
     * @return QueryBuilder
128
     */
129
    public function getQueryBuilder()
130
    {
131
        return new QueryBuilder($this);
132
    }
133
134
    /**
135
     * Performs GET HTTP request.
136
     * @param string $url     URL
137
     * @param array  $options URL options
138
     * @param string $body    request body
139
     * @param bool   $raw     if response body contains JSON and should be decoded
140
     * @throws HiArtException
141
     * @throws \yii\base\InvalidConfigException
142
     * @return mixed response
143
     */
144
    public function get($url, $options = [], $body = null, $raw = false)
145
    {
146
        return $this->makeRequest('GET', $url, $options, $body, $raw);
147
    }
148
149
    /**
150
     * Performs HEAD HTTP request.
151
     * @param string $url     URL
152
     * @param array  $options URL options
153
     * @param string $body    request body
154
     * @throws HiArtException
155
     * @throws \yii\base\InvalidConfigException
156
     * @return mixed response
157
     */
158
    public function head($url, $options = [], $body = null)
159
    {
160
        return $this->makeRequest('HEAD', $url, $options, $body, $raw);
0 ignored issues
show
Bug introduced by
The variable $raw does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
161
    }
162
163
    /**
164
     * Performs POST HTTP request.
165
     * @param string $url     URL
166
     * @param array  $options URL options
167
     * @param string $body    request body
168
     * @param bool   $raw     if response body contains JSON and should be decoded
169
     * @throws HiArtException
170
     * @throws \yii\base\InvalidConfigException
171
     * @return mixed response
172
     */
173
    public function post($url, $options = [], $body = null, $raw = false)
174
    {
175
        return $this->makeRequest('POST', $url, $options, $body, $raw);
176
    }
177
178
    /**
179
     * Performs PUT HTTP request.
180
     * @param string $url     URL
181
     * @param array  $options URL options
182
     * @param string $body    request body
183
     * @param bool   $raw     if response body contains JSON and should be decoded
184
     * @throws HiArtException
185
     * @throws \yii\base\InvalidConfigException
186
     * @return mixed response
187
     */
188
    public function put($url, $options = [], $body = null, $raw = false)
189
    {
190
        return $this->makeRequest('PUT', $url, $options, $body, $raw);
191
    }
192
193
    /**
194
     * Performs DELETE HTTP request.
195
     * @param string $url     URL
196
     * @param array  $options URL options
197
     * @param string $body    request body
198
     * @param bool   $raw     if response body contains JSON and should be decoded
199
     * @throws HiArtException
200
     * @throws \yii\base\InvalidConfigException
201
     * @return mixed response
202
     */
203
    public function delete($url, $options = [], $body = null, $raw = false)
204
    {
205
        return $this->makeRequest('DELETE', $url, $options, $body, $raw);
206
    }
207
208
    /**
209
     * XXX To be removed in favour of post().
210
     * @param $url
211
     * @param array $options
212
     * @return mixed
213
     */
214
    public function perform($url, $options = [])
215
    {
216
        return $this->makeRequest('DELETE', $url, $options);
217
    }
218
219
    /**
220
     * Make request and check for error.
221
     * @param string $url     URL
222
     * @param array  $options URL options
223
     * @param string $body    request body
224
     * @param bool   $raw     if response body contains JSON and should be decoded
225
     * @throws HiArtException
226
     * @throws \yii\base\InvalidConfigException
227
     * @return mixed response
228
     */
229
    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...
230
    {
231
        #$result = $this->curlRequest($method, $this->createUrl($url), http_build_query($options), $raw);
0 ignored issues
show
Unused Code Comprehensibility introduced by
68% 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...
232
        $result = $this->guzzleRequest($method, $this->createUrl($url), $options, $raw);
233
234
        return $this->checkResponse($result, $url, $options);
235
    }
236
237
    /**
238
     * Creates URL.
239
     * @param mixed $path path
240
     * @param array $options URL options
241
     * @return array
242
     */
243
    private function createUrl($path, array $options = [])
244
    {
245
        $options = array_merge($this->getAuth(), $options);
246
        if (!is_string($path)) {
247
            $url = urldecode(reset($path));
248
            if (!empty($options)) {
249
                $url .= '?' . http_build_query($options);
250
            }
251
        } else {
252
            $url = $path;
253
            if (!empty($options)) {
254
                $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($options);
255
            }
256
        }
257
258
        return [$this->config['api_url'], $url];
259
    }
260
261
    protected function guzzleRequest($method, $url, $body = null, $raw = false)
262
    {
263
        $method  = strtoupper($method);
264
        $profile = $method . ' ' . $url[1] . '#' . (is_array($body) ? http_build_query($body) : $body);
265
        $options = [(is_array($body) ? 'form_params' : 'body') => $body];
266
        Yii::beginProfile($profile, __METHOD__);
267
        $response = $this->getGuzzle()->request($method, $url[1], $options);
268
        Yii::endProfile($profile, __METHOD__);
269
270
        $res = $response->getBody()->getContents();
271
        if (!$raw && in_array('application/json', $response->getHeader('Content-Type'))) {
272
            $res = Json::decode($res);
273
        }
274
275
        return $res;
276
    }
277
278
    public function getGuzzle()
279
    {
280
        if (static::$guzzle === null) {
281
            static::$guzzle = new \GuzzleHttp\Client($this->config);
282
        }
283
284
        return static::$guzzle;
285
    }
286
287
    /**
288
     * Performs HTTP request.
289
     * @param string $method method name
290
     * @param string $url URL
291
     * @param string $requestBody request body
292
     * @param bool $raw if response body contains JSON and should be decoded
293
     * @throws ErrorResponseException
294
     * @throws HiArtException
295
     * @return mixed if request failed
296
     */
297
    protected function curlRequest($method, $url, $requestBody = null, $raw = false)
298
    {
299
        $method = strtoupper($method);
300
        // response body and headers
301
        $headers = [];
302
        $body    = '';
303
        $options = [
304
            CURLOPT_URL       => $url,
305
            CURLOPT_USERAGENT => 'Yii Framework ' . Yii::getVersion() . ' (HiArt)',
306
            //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...
307
            # 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...
308
            CURLOPT_RETURNTRANSFER => false,
309
            CURLOPT_HEADER         => false,
310
            CURLOPT_SSL_VERIFYPEER => false,
311
            CURLOPT_SSL_VERIFYHOST => 2,
312
            // http://www.php.net/manual/en/function.curl-setopt.php#82418
313
            CURLOPT_HTTPHEADER    => ['Expect:'],
314
            CURLOPT_WRITEFUNCTION => function ($curl, $data) use (&$body) {
315
                $body .= $data;
316
317
                return mb_strlen($data, '8bit');
318
            },
319
            CURLOPT_HEADERFUNCTION => function ($curl, $data) use (&$headers) {
320
                foreach (explode("\r\n", $data) as $row) {
321
                    if (($pos = strpos($row, ':')) !== false) {
322
                        $headers[strtolower(substr($row, 0, $pos))] = trim(substr($row, $pos + 1));
323
                    }
324
                }
325
326
                return mb_strlen($data, '8bit');
327
            },
328
            CURLOPT_CUSTOMREQUEST => $method,
329
        ];
330
        if ($this->connectionTimeout !== null) {
331
            $options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout;
332
        }
333
        if ($this->dataTimeout !== null) {
334
            $options[CURLOPT_TIMEOUT] = $this->dataTimeout;
335
        }
336
        if ($requestBody !== null) {
337
            $options[CURLOPT_POSTFIELDS] = $requestBody;
338
        }
339
        if ($method === 'HEAD') {
340
            $options[CURLOPT_NOBODY] = true;
341
            unset($options[CURLOPT_WRITEFUNCTION]);
342
        }
343
        if (is_array($url)) {
344
            list($host, $q) = $url;
345
            if (strncmp($host, 'inet[', 5) === 0) {
346
                $host = substr($host, 5, -1);
347
                if (($pos = strpos($host, '/')) !== false) {
348
                    $host = substr($host, $pos + 1);
349
                }
350
            }
351
            $profile = $method . ' ' . $q . '#' . $requestBody;
352
            if (preg_match('@^https?://@', $host)) {
353
                $url = $host . '/' . $q;
354
            } else {
355
                throw new HiArtException('Request failed: please specify the protocol (http, https) in reference to the API HiResource Core');
356
            }
357
        } else {
358
            $profile = false;
359
        }
360
        $options[CURLOPT_URL] = $url;
361
        Yii::trace("Sending request to node: $method $url\n$requestBody", __METHOD__);
362
        if ($profile !== false) {
363
            Yii::beginProfile($profile, __METHOD__);
364
        }
365
        $curl = $this->getHandler();
366
        curl_setopt_array($curl, $options);
367
        if (curl_exec($curl) === false) {
368
            throw new HiArtException('Request failed: ' . curl_errno($curl) . ' - ' . curl_error($curl), [
369
                'requestUrl'      => $url,
370
                'requestBody'     => $requestBody,
371
                'responseBody'    => $this->decodeErrorBody($body),
372
                'requestMethod'   => $method,
373
                'responseHeaders' => $headers,
374
            ]);
375
        }
376
377
        $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
378
        Yii::trace(curl_getinfo($curl));
379
        if ($profile !== false) {
380
            Yii::endProfile($profile, __METHOD__);
381
        }
382
        if ($responseCode >= 200 && $responseCode < 300) {
383
            if ($method === 'HEAD') {
384
                return true;
385
            } else {
386
                if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) {
387
                    throw new HiArtException("Incomplete data received: $len < {$headers['content-length']}", [
388
                        'requestMethod'   => $method,
389
                        'requestUrl'      => $url,
390
                        'requestBody'     => $requestBody,
391
                        'responseCode'    => $responseCode,
392
                        'responseHeaders' => $headers,
393
                        'responseBody'    => $this->decodeErrorBody($body),
394
                    ]);
395
                }
396
                if (isset($headers['content-type']) && !strncmp($headers['content-type'], 'application/json', 16)) {
397
                    return $raw ? $body : Json::decode($body);
398
                } else {
399
                    return $body;
400
                }
401
                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...
402
                    'requestUrl'      => $url,
403
                    'requestBody'     => $requestBody,
404
                    'responseBody'    => $this->decodeErrorBody($body),
405
                    'requestMethod'   => $method,
406
                    'responseCode'    => $responseCode,
407
                    'responseHeaders' => $headers,
408
                ]);
409
            }
410
        } elseif ($responseCode === 404) {
411
            return false;
412
        } else {
413
            throw new HiArtException("Request request failed with code $responseCode.", [
414
                'requestUrl'      => $url,
415
                'requestBody'     => $requestBody,
416
                'responseBody'    => $this->decodeErrorBody($body),
417
                'requestMethod'   => $method,
418
                'responseCode'    => $responseCode,
419
                'responseHeaders' => $headers,
420
            ]);
421
        }
422
    }
423
424
    /**
425
     * Try to decode error information if it is valid json, return it if not.
426
     * @param $body
427
     * @return mixed
428
     */
429
    protected function decodeErrorBody($body)
430
    {
431
        try {
432
            $decoded = Json::decode($body);
433
            if (isset($decoded['error'])) {
434
                $decoded['error'] = preg_replace('/\b\w+?Exception\[/',
435
                    "<span style=\"color: red;\">\\0</span>\n               ", $decoded['error']);
436
            }
437
438
            return $decoded;
439
        } catch (InvalidParamException $e) {
440
            return $body;
441
        }
442
    }
443
444
    /**
445
     * Callback to test if API response has error.
446
     */
447
    public $errorChecker;
448
449
    /**
450
     * Checks response with errorChecker callback and raises exception if error.
451
     * @param array  $response response data from API
452
     * @param string $url      request URL
453
     * @param array  $options  request data
454
     * @throws ErrorResponseException
455
     * @return array
456
     */
457
    protected function checkResponse($response, $url, $options)
458
    {
459
        $error = call_user_func($this->errorChecker, $response);
460
        if ($error) {
461
            throw new ErrorResponseException($error, [
462
                'requestUrl' => $url,
463
                'request'    => $options,
464
                'response'   => $response,
465
            ]);
466
        }
467
468
        return $response;
469
    }
470
}
471