Completed
Push — master ( 25fc39...0c3403 )
by ARCANEDEV
29s
created

HttpClient::close()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 0
ccs 1
cts 1
cp 1
crap 2
rs 9.4285
1
<?php namespace Arcanedev\Stripe\Http\Curl;
2
3
use Arcanedev\Stripe\Contracts\Http\Curl\HttpClientInterface;
4
use Arcanedev\Stripe\Exceptions\ApiConnectionException;
5
6
/**
7
 * Class     HttpClient
8
 *
9
 * @package  Arcanedev\Stripe\Http\Curl
10
 * @author   ARCANEDEV <[email protected]>
11
 */
12
class HttpClient implements HttpClientInterface
13
{
14
    /* ------------------------------------------------------------------------------------------------
15
     |  Constants
16
     | ------------------------------------------------------------------------------------------------
17
     */
18
    const DEFAULT_TIMEOUT = 80;
19
    const DEFAULT_CONNECT_TIMEOUT = 30;
20
21
    /* ------------------------------------------------------------------------------------------------
22
     |  Properties
23
     | ------------------------------------------------------------------------------------------------
24
     */
25
    /**
26
     * The HTTP Client instance.
27
     *
28
     * @var \Arcanedev\Stripe\Http\Curl\HttpClient
29
     */
30
    private static $instance;
31
32
    /**
33
     * @var string
34
     */
35
    private $apiKey;
36
37
    /**
38
     * @var string
39
     */
40
    private $apiBaseUrl;
41
42
    /**
43
     * @var \Arcanedev\Stripe\Http\Curl\HeaderBag
44
     */
45
    private $headers;
46
47
    /**
48
     * @var \Arcanedev\Stripe\Http\Curl\CurlOptions
49
     */
50
    private $options;
51
52
    /**
53
     * @var resource
54
     */
55
    private $curl;
56
57
    /**
58
     * @var int
59
     */
60
    private $timeout = self::DEFAULT_TIMEOUT;
61
62
    /**
63
     * @var int
64
     */
65
    private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
66
67
    /**
68
     * @var mixed
69
     */
70
    private $response;
71
72
    /**
73
     * @var int
74
     */
75
    private $errorCode;
76
77
    /**
78
     * @var string
79
     */
80
    private $errorMessage;
81
82
    /* ------------------------------------------------------------------------------------------------
83
     |  Constructor & Destructor
84
     | ------------------------------------------------------------------------------------------------
85
     */
86
    /**
87
     * Create a HttpClient instance.
88
     */
89 5
    private function __construct()
90
    {
91 5
        $this->headers  = new HeaderBag;
92 5
        $this->options  = new CurlOptions;
93 5
        $this->response = null;
94 5
    }
95
96
    /**
97
     * Destroy the instance.
98
     */
99
    public function __destruct()
100
    {
101
        $this->close();
102
    }
103
104
    /* ------------------------------------------------------------------------------------------------
105
     |  Getters & Setters
106
     | ------------------------------------------------------------------------------------------------
107
     */
108
    /**
109
     * Set API Key.
110
     *
111
     * @param  string  $apiKey
112
     *
113
     * @return self
114
     */
115 744
    public function setApiKey($apiKey)
116
    {
117 744
        $this->apiKey = $apiKey;
118
119 744
        return $this;
120
    }
121
122
    /**
123
     * Set Base URL.
124
     *
125
     * @param  string  $apiBaseUrl
126
     *
127
     * @return self
128
     */
129
    public function setApiBaseUrl($apiBaseUrl)
130
    {
131
        $this->apiBaseUrl = $apiBaseUrl;
132
133
        return $this;
134
    }
135
136
    /**
137
     * Get the timeout.
138
     *
139
     * @return int
140
     */
141 5
    public function getTimeout()
142
    {
143 5
        return $this->timeout;
144
    }
145
146
    /**
147
     * Set the timeout.
148
     *
149
     * @param  int  $seconds
150
     *
151
     * @return self
152
     */
153 1434
    public function setTimeout($seconds)
154
    {
155 1434
        $this->timeout = (int) max($seconds, 0);
156
157 1434
        return $this;
158
    }
159
160
    /**
161
     * Get the connect timeout.
162
     *
163
     * @return int
164
     */
165 5
    public function getConnectTimeout()
166
    {
167 5
        return $this->connectTimeout;
168
    }
169
170
    /**
171
     * Set the connect timeout.
172
     *
173
     * @param  int  $seconds
174
     *
175
     * @return self
176
     */
177 5
    public function setConnectTimeout($seconds)
178
    {
179 5
        $this->connectTimeout = (int) max($seconds, 0);
180
181 5
        return $this;
182
    }
183
184
    /**
185
     * Get array options.
186
     *
187
     * @return array
188
     */
189
    public function getOptions()
190
    {
191 744
        return $this->options->get();
192
    }
193 744
194
    /**
195 744
     * Set array options.
196
     *
197
     * @param  array  $options
198
     *
199
     * @return self
200
     */
201
    public function setOptionArray(array $options)
202
    {
203
        $this->options->setOptions($options);
204
205 744
        return $this;
206
    }
207 744
208 744
    /* ------------------------------------------------------------------------------------------------
209
     |  Curl Functions
210
     | ------------------------------------------------------------------------------------------------
211
     */
212
    /**
213 744
     * Init curl.
214
     */
215 744
    private function init()
216 744
    {
217 744
        $this->curl = curl_init();
218 744
    }
219
220
    /**
221
     * Execute curl.
222
     */
223 744
    private function execute()
224
    {
225 744
        curl_setopt_array($this->curl, $this->getOptions());
226 744
        $this->response     = curl_exec($this->curl);
227 595
        $this->errorCode    = curl_errno($this->curl);
228 744
        $this->errorMessage = curl_error($this->curl);
229
    }
230
231
    /**
232
     * Close curl.
233
     */
234
    private function close()
235
    {
236
        if (is_resource($this->curl)) {
237
            curl_close($this->curl);
238
        }
239 1434
    }
240
241 1434
    /* ------------------------------------------------------------------------------------------------
242 5
     |  Main Functions
243 4
     | ------------------------------------------------------------------------------------------------
244
     */
245 1434
    /**
246
     * Make the HTTP Client with options.
247
     *
248
     * @param  array  $options
249
     *
250
     * @return static
251
     */
252
    public static function make(array $options = [])
253
    {
254
        return (new static)->setOptionArray($options);
255
    }
256
257
    /**
258
     * Get the HTTP.
259 744
     *
260
     * @return self
261 744
     */
262 520
    public static function instance()
263 416
    {
264
        if ( ! self::$instance) {
265 629
            self::$instance = new self;
266
        }
267
268 744
        return self::$instance;
269 744
    }
270 744
271 744
    /**
272 744
     * Curl the request.
273 595
     *
274
     * @param  string        $method
275 744
     * @param  string        $url
276 744
     * @param  array|string  $params
277
     * @param  array         $headers
278 744
     * @param  bool          $hasFile
279 744
     *
280 744
     * @return array
281
     */
282 744
    public function request($method, $url, $params, $headers, $hasFile)
283 744
    {
284
        if ($method !== 'post') {
285 744
            $url    = str_parse_url($url, $params);
286 744
        }
287
        else {
288 744
            $params = $hasFile ? $params : self::encode($params);
289
        }
290
291
        $this->headers->prepare($this->apiKey, $headers, $hasFile);
292
        $this->options->make($method, $url, $params, $this->headers->get(), $hasFile);
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type array; however, Arcanedev\Stripe\Http\Curl\CurlOptions::make() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
293
        $this->setOptionArray([
294 744
            CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
295
            CURLOPT_TIMEOUT        => $this->timeout,
296 744
        ]);
297
298
        $respHeaders = [];
299
        $this->prepareResponseHeaders($respHeaders);
300
301
        $this->init();
302
        $this->execute();
303
        $this->checkCertErrors();
304
        $this->checkResponse();
305
306
        $statusCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE);
307
        $this->close();
308
309 744
        return [$this->response, $statusCode, $respHeaders];
310
    }
311
312
    /**
313
     * Check Cert Errors.
314
     */
315
    private function checkCertErrors()
316
    {
317
        if (SslChecker::hasCertErrors($this->errorCode)) {
318
            $this->headers->set(
319 620
                'X-Stripe-Client-Info',
320
                '{"ca":"using Stripe-supplied CA bundle"}'
321
            );
322
323
            $this->setOptionArray([
324
                CURLOPT_HTTPHEADER => $this->headers->get(),
325 620
                CURLOPT_CAINFO     => SslChecker::caBundle()
326
            ]);
327 620
328 610
            $this->execute();
329
        }
330 610
    }
331 510
332 510
    /**
333 510
     * Encode array to query string
334 408
     *
335
     * @param  array|string  $array
336 610
     * @param  string|null   $prefix
337 610
     *
338 488
     * @return string
339 510
     */
340 530
    protected static function encode($array, $prefix = null)
341 408
    {
342 496
        // @codeCoverageIgnoreStart
343
        if ( ! is_array($array)) return $array;
344 620
        // @codeCoverageIgnoreEnd
345
346
        $result = [];
347
348
        foreach ($array as $key => $value) {
349
            if (is_null($value)) continue;
350
351
            if ($prefix) {
352
                $key = ($key !== null && (! is_int($key) || is_array($value)))
353
                    ? $prefix . "[" . $key . "]"
354
                    : $prefix . "[]";
355
            }
356 744
357
            if ( ! is_array($value)) {
358 744
                $result[] = urlencode($key) . '=' . urlencode($value);
359
            }
360
            elseif ($enc = self::encode($value, $key)) {
361
                $result[] = $enc;
362
            }
363
        }
364
365
        return implode('&', $result);
366
    }
367
368
    /* ------------------------------------------------------------------------------------------------
369
     |  Other Functions
370
     | ------------------------------------------------------------------------------------------------
371
     */
372
    /**
373
     * Check Response.
374
     *
375
     * @throws \Arcanedev\Stripe\Exceptions\ApiConnectionException
376
     */
377
    private function checkResponse()
378
    {
379
        if ($this->response !== false) return;
380
381
        $this->close();
382
        $this->handleCurlError();
383
    }
384
385
    /**
386
     * Handle CURL errors.
387
     *
388
     * @throws \Arcanedev\Stripe\Exceptions\ApiConnectionException
389
     */
390
    private function handleCurlError()
391
    {
392
        switch ($this->errorCode) {
393
            case CURLE_COULDNT_CONNECT:
394
            case CURLE_COULDNT_RESOLVE_HOST:
395
            case CURLE_OPERATION_TIMEOUTED:
396
                $msg = "Could not connect to Stripe ({$this->apiBaseUrl}). Please check your internet connection "
397
                    . 'and try again.  If this problem persists, you should check Stripe\'s service status at '
398
                    . 'https://twitter.com/stripestatus, or';
399
                break;
400
401
            case CURLE_SSL_CACERT:
402
            case CURLE_SSL_PEER_CERTIFICATE:
403
                $msg = 'Could not verify Stripe\'s SSL certificate.  Please make sure that your network is not '
404
                    . "intercepting certificates. (Try going to {$this->apiBaseUrl} in your browser.) "
405
                    . 'If this problem persists,';
406 744
                break;
407
408 744
            default:
409 744
                $msg = 'Unexpected error communicating with Stripe. If this problem persists,';
410
                // no break
411
        }
412 744
413 744
        $msg .= ' let us know at [email protected].';
414 744
415 744
        $msg .= "\n\n(Network error [errno {$this->errorCode}]: {$this->errorMessage})";
416 744
417
        throw new ApiConnectionException($msg);
418
    }
419
420
    /**
421
     * Prepare Response Headers.
422
     *
423
     * @return array
424
     */
425
    private function prepareResponseHeaders(array &$respHeaders)
426
    {
427
        $this->options->setOption(CURLOPT_HEADERFUNCTION, function ($curl, $header_line) use (&$respHeaders) {
428
            // Ignore the HTTP request line (HTTP/1.1 200 OK)
429
            if (strpos($header_line, ":") === false) {
430
                return strlen($header_line);
431
            }
432
433
            list($key, $value) = explode(":", trim($header_line), 2);
434
            $respHeaders[trim($key)] = trim($value);
435
            return strlen($header_line);
436
        });
437
    }
438
}
439