Completed
Push — master ( 5cf718...0d45dc )
by ARCANEDEV
20:31
created

HttpClient   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 492
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Test Coverage

Coverage 70.59%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 492
wmc 51
lcom 2
cbo 5
ccs 108
cts 153
cp 0.7059
rs 8.3206

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __destruct() 0 4 1
A getTimeout() 0 4 1
A getConnectTimeout() 0 4 1
A init() 0 4 1
A processResourceParams() 0 18 4
A __construct() 0 6 1
A setApiKey() 0 6 1
A setApiBaseUrl() 0 6 1
A setTimeout() 0 6 1
A setConnectTimeout() 0 6 1
A setOptionArray() 0 6 1
A execute() 0 6 1
A close() 0 6 2
A instance() 0 8 2
B request() 0 30 3
A checkCertErrors() 0 16 2
A processResourceParam() 0 13 2
D encode() 0 30 10
A checkResourceType() 0 8 2
A checkResourceMetaData() 0 8 2
A checkHasResourceFile() 0 6 3
A checkResponse() 0 7 2
B handleCurlError() 0 29 6

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 namespace Arcanedev\Stripe\Http\Curl;
2
3
use Arcanedev\Stripe\Contracts\Http\Curl\HttpClientInterface;
4
use Arcanedev\Stripe\Exceptions\ApiConnectionException;
5
use Arcanedev\Stripe\Exceptions\ApiException;
6
use CURLFile;
7
8
/**
9
 * Class     HttpClient
10
 *
11
 * @package  Arcanedev\Stripe\Utilities\Request
12
 * @author   ARCANEDEV <[email protected]>
13
 */
14
class HttpClient implements HttpClientInterface
15
{
16
    /* ------------------------------------------------------------------------------------------------
17
     |  Constants
18
     | ------------------------------------------------------------------------------------------------
19
     */
20
    const DEFAULT_TIMEOUT = 80;
21
    const DEFAULT_CONNECT_TIMEOUT = 30;
22
23
    /* ------------------------------------------------------------------------------------------------
24
     |  Properties
25
     | ------------------------------------------------------------------------------------------------
26
     */
27
    /**
28
     * The HTTP Client instance.
29
     *
30
     * @var HttpClient
31
     */
32
    private static $instance;
33
34
    /**
35
     * @var string
36
     */
37
    private $apiKey;
38
39
    /**
40
     * @var string
41
     */
42
    private $apiBaseUrl;
43
44
    /**
45
     * @var HeaderBag
46
     */
47
    private $headers;
48
49
    /**
50
     * @var CurlOptions
51
     */
52
    private $options;
53
54
    /**
55
     * @var resource
56
     */
57
    private $curl;
58
59
    /**
60
     * @var int
61
     */
62
    private $timeout = self::DEFAULT_TIMEOUT;
63
64
    /**
65
     * @var int
66
     */
67
    private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
68
69
    /**
70
     * @var mixed
71
     */
72
    private $response;
73
74
    /**
75
     * @var int
76
     */
77
    private $errorCode;
78
79
    /**
80
     * @var string
81
     */
82
    private $errorMessage;
83
84
    /* ------------------------------------------------------------------------------------------------
85
     |  Constructor & Destructor
86
     | ------------------------------------------------------------------------------------------------
87
     */
88
    /**
89
     * Create a HttpClient instance.
90
     */
91 5
    private function __construct()
92
    {
93 5
        $this->headers  = new HeaderBag;
94 5
        $this->options  = new CurlOptions;
95 5
        $this->response = null;
96 5
    }
97
98
    /**
99
     * Destroy the instance.
100
     */
101
    public function __destruct()
102
    {
103
        $this->close();
104
    }
105
106
    /* ------------------------------------------------------------------------------------------------
107
     |  Getters & Setters
108
     | ------------------------------------------------------------------------------------------------
109
     */
110
    /**
111
     * Set API Key.
112
     *
113
     * @param  string  $apiKey
114
     *
115
     * @return self
116
     */
117 619
    public function setApiKey($apiKey)
118
    {
119 619
        $this->apiKey = $apiKey;
120
121 619
        return $this;
122
    }
123
124
    /**
125
     * Set Base URL.
126
     *
127
     * @param  string  $apiBaseUrl
128
     *
129
     * @return self
130
     */
131
    public function setApiBaseUrl($apiBaseUrl)
132
    {
133
        $this->apiBaseUrl = $apiBaseUrl;
134
135
        return $this;
136
    }
137
138
    /**
139
     * Get the timeout.
140
     *
141
     * @return int
142
     */
143 5
    public function getTimeout()
144
    {
145 5
        return $this->timeout;
146
    }
147
148
    /**
149
     * Set the timeout.
150
     *
151
     * @param  int  $seconds
152
     *
153
     * @return self
154
     */
155 1244
    public function setTimeout($seconds)
156
    {
157 1244
        $this->timeout = (int) max($seconds, 0);
158
159 1244
        return $this;
160
    }
161
162
    /**
163
     * Get the connect timeout.
164
     *
165
     * @return int
166
     */
167 5
    public function getConnectTimeout()
168
    {
169 5
        return $this->connectTimeout;
170
    }
171
172
    /**
173
     * Set the connect timeout.
174
     *
175
     * @param  int  $seconds
176
     *
177
     * @return self
178
     */
179 5
    public function setConnectTimeout($seconds)
180
    {
181 5
        $this->connectTimeout = (int) max($seconds, 0);
182
183 5
        return $this;
184
    }
185
186
    /**
187
     * Set array options.
188
     *
189
     * @param  array  $options
190
     *
191
     * @return self
192
     */
193 619
    public function setOptionArray(array $options)
194
    {
195 619
        curl_setopt_array($this->curl, $options);
196
197 619
        return $this;
198
    }
199
200
    /* ------------------------------------------------------------------------------------------------
201
     |  Curl Functions
202
     | ------------------------------------------------------------------------------------------------
203
     */
204
    /**
205
     * Init curl.
206
     */
207 619
    private function init()
208
    {
209 619
        $this->curl = curl_init();
210 619
    }
211
212
    /**
213
     * Execute curl.
214
     */
215 619
    private function execute()
216
    {
217 619
        $this->response     = curl_exec($this->curl);
218 619
        $this->errorCode    = curl_errno($this->curl);
219 619
        $this->errorMessage = curl_error($this->curl);
220 619
    }
221
222
    /**
223
     * Close curl.
224
     */
225 619
    private function close()
226
    {
227 619
        if (is_resource($this->curl)) {
228 619
            curl_close($this->curl);
229 495
        }
230 619
    }
231
232
    /* ------------------------------------------------------------------------------------------------
233
     |  Main Functions
234
     | ------------------------------------------------------------------------------------------------
235
     */
236
    /**
237
     * Get the HTTP.
238
     *
239
     * @return self
240
     */
241 1244
    public static function instance()
242
    {
243 1244
        if ( ! self::$instance) {
244 5
            self::$instance = new self;
245 4
        }
246
247 1244
        return self::$instance;
248
    }
249
250
    /**
251
     * Curl the request.
252
     *
253
     * @param  string        $method
254
     * @param  string        $url
255
     * @param  array|string  $params
256
     * @param  array         $headers
257
     *
258
     * @throws ApiConnectionException
259
     * @throws ApiException
260
     *
261
     * @return array
262
     */
263 619
    public function request($method, $url, $params, $headers)
264
    {
265 619
        $hasFile = self::processResourceParams($params);
266
267 619
        if ($method !== 'post') {
268 435
            $url    = str_parse_url($url, $params);
269 348
        }
270
        else {
271 529
            $params = $hasFile ? $params : str_url_queries($params);
272
        }
273
274 619
        $this->headers->prepare($this->apiKey, $headers, $hasFile);
275 619
        $this->options->make($method, $url, $params, $this->headers->get(), $hasFile);
276 619
        $this->options->setOptions([
277 619
            CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
278 619
            CURLOPT_TIMEOUT        => $this->timeout,
279 495
        ]);
280
281 619
        $this->init();
282 619
        $this->setOptionArray($this->options->get());
283 619
        $this->execute();
284
285 619
        $this->checkCertErrors();
286 619
        $this->checkResponse();
287
288 619
        $statusCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE);
289 619
        $this->close();
290
291 619
        return [$this->response, $statusCode];
292
    }
293
294
    /**
295
     * Check Cert Errors.
296
     */
297 619
    private function checkCertErrors()
298
    {
299 619
        if (SslChecker::hasCertErrors($this->errorCode)) {
300
            $this->headers->set(
301
                'X-Stripe-Client-Info',
302
                '{"ca":"using Stripe-supplied CA bundle"}'
303
            );
304
305
            $this->setOptionArray([
306
                CURLOPT_HTTPHEADER => $this->headers->get(),
307
                CURLOPT_CAINFO     => SslChecker::caBundle()
308
            ]);
309
310
            $this->execute();
311
        }
312 619
    }
313
314
    /**
315
     * Process Resource Parameters.
316
     *
317
     * @param  array|string  $params
318
     *
319
     * @throws ApiException
320
     *
321
     * @return bool
322
     */
323 619
    private static function processResourceParams(&$params)
324
    {
325
        // @codeCoverageIgnoreStart
326
        if ( ! is_array($params)) return false;
327
        // @codeCoverageIgnoreEnd
328
329 619
        $hasFile = false;
330
331 619
        foreach ($params as $key => $resource) {
332 524
            $hasFile = self::checkHasResourceFile($resource);
333
334 524
            if (is_resource($resource)) {
335 113
                $params[$key] = self::processResourceParam($resource);
336 8
            }
337 495
        }
338
339 619
        return $hasFile;
340
    }
341
342
    /**
343
     * Process Resource Parameter.
344
     *
345
     * @param  resource  $resource
346
     *
347
     * @throws ApiException
348
     *
349
     * @return CURLFile|string
350
     */
351 10
    private static function processResourceParam($resource)
352
    {
353 10
        self::checkResourceType($resource);
354
355 10
        $metaData = stream_get_meta_data($resource);
356
357 10
        self::checkResourceMetaData($metaData);
358
359
        // We don't have the filename or mimetype, but the API doesn't care
360 10
        return class_exists('CURLFile')
361 10
            ? new CURLFile($metaData['uri'])
362 10
            : '@' . $metaData['uri'];
363
    }
364
365
    /**
366
     * Encode array to query string
367
     *
368
     * @param  array        $array
369
     * @param  string|null  $prefix
370
     *
371
     * @return string
372
     */
373 5
    protected static function encode($array, $prefix = null)
374
    {
375
        // @codeCoverageIgnoreStart
376
        if ( ! is_array($array)) return $array;
377
        // @codeCoverageIgnoreEnd
378
379 5
        $result = [];
380
381 5
        foreach ($array as $key => $value) {
382 5
            if (is_null($value)) {
383 5
                continue;
384
            }
385
386 5
            if ($prefix && $key && ! is_int($key)) {
387 5
                $key = $prefix .'[' . $key . ']';
388 4
            }
389 5
            elseif ($prefix) {
390 5
                $key = $prefix . '[]';
391 4
            }
392
393 5
            if ( ! is_array($value)) {
394 5
                $result[] = urlencode($key) . '=' . urlencode($value);
395 4
            }
396 5
            elseif ($enc = self::encode($value, $key)) {
397 5
                $result[] = $enc;
398 4
            }
399 4
        }
400
401 5
        return implode('&', $result);
402
    }
403
404
    /* ------------------------------------------------------------------------------------------------
405
     |  Check Functions
406
     | ------------------------------------------------------------------------------------------------
407
     */
408
    /**
409
     * Check Resource type is stream.
410
     *
411
     * @param  resource  $resource
412
     *
413
     * @throws ApiException
414
     */
415 10
    private static function checkResourceType($resource)
416
    {
417 10
        if (get_resource_type($resource) !== 'stream') {
418
            throw new ApiException(
419
                'Attempted to upload a resource that is not a stream'
420
            );
421
        }
422 10
    }
423
424
    /**
425
     * Check resource MetaData.
426
     *
427
     * @param  array  $metaData
428
     *
429
     * @throws ApiException
430
     */
431 10
    private static function checkResourceMetaData(array $metaData)
432
    {
433 10
        if ($metaData['wrapper_type'] !== 'plainfile') {
434
            throw new ApiException(
435
                'Only plainfile resource streams are supported'
436
            );
437
        }
438 10
    }
439
440
    /**
441
     * Check if param is resource File.
442
     *
443
     * @param  mixed  $resource
444
     *
445
     * @return bool
446
     */
447 524
    private static function checkHasResourceFile($resource)
448
    {
449
        return
450 524
            is_resource($resource) ||
451 524
            (class_exists('CURLFile') && $resource instanceof CURLFile);
452
    }
453
454
    /* ------------------------------------------------------------------------------------------------
455
     |  Other Functions
456
     | ------------------------------------------------------------------------------------------------
457
     */
458
    /**
459
     * Check Response.
460
     *
461
     * @throws ApiConnectionException
462
     */
463 619
    private function checkResponse()
464
    {
465 619
        if ($this->response !== false) return;
466
467
        $this->close();
468
        $this->handleCurlError();
469
    }
470
471
    /**
472
     * Handle CURL errors.
473
     *
474
     * @throws ApiConnectionException
475
     */
476
    private function handleCurlError()
477
    {
478
        switch ($this->errorCode) {
479
            case CURLE_COULDNT_CONNECT:
480
            case CURLE_COULDNT_RESOLVE_HOST:
481
            case CURLE_OPERATION_TIMEOUTED:
482
                $msg = 'Could not connect to Stripe (' . $this->apiBaseUrl . '). Please check your internet connection '
483
                    . 'and try again.  If this problem persists, you should check Stripe\'s service status at '
484
                    . 'https://twitter.com/stripestatus, or';
485
                break;
486
487
            case CURLE_SSL_CACERT:
488
            case CURLE_SSL_PEER_CERTIFICATE:
489
                $msg = 'Could not verify Stripe\'s SSL certificate.  Please make sure that your network is not '
490
                    . 'intercepting certificates. (Try going to ' . $this->apiBaseUrl . ' in your browser.) '
491
                    . 'If this problem persists,';
492
                break;
493
494
            default:
495
                $msg = 'Unexpected error communicating with Stripe. If this problem persists,';
496
                // no break
497
        }
498
499
        $msg .= ' let us know at [email protected].';
500
501
        $msg .= "\n\n(Network error [errno {$this->errorCode}]: {$this->errorMessage})";
502
503
        throw new ApiConnectionException($msg);
504
    }
505
}
506