Completed
Pull Request — master (#13)
by ARCANEDEV
08:08
created

HttpClient::checkResourceMetaData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 3
cts 6
cp 0.5
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 2.5
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\Utilities\Request
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 644
    public function setApiKey($apiKey)
116
    {
117 644
        $this->apiKey = $apiKey;
118
119 644
        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 1289
    public function setTimeout($seconds)
154
    {
155 1289
        $this->timeout = (int) max($seconds, 0);
156
157 1289
        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
     * Set array options.
186
     *
187
     * @param  array  $options
188
     *
189
     * @return self
190
     */
191 644
    public function setOptionArray(array $options)
192
    {
193 644
        curl_setopt_array($this->curl, $options);
194
195 644
        return $this;
196
    }
197
198
    /* ------------------------------------------------------------------------------------------------
199
     |  Curl Functions
200
     | ------------------------------------------------------------------------------------------------
201
     */
202
    /**
203
     * Init curl.
204
     */
205 644
    private function init()
206
    {
207 644
        $this->curl = curl_init();
208 644
    }
209
210
    /**
211
     * Execute curl.
212
     */
213 644
    private function execute()
214
    {
215 644
        $this->response     = curl_exec($this->curl);
216 644
        $this->errorCode    = curl_errno($this->curl);
217 644
        $this->errorMessage = curl_error($this->curl);
218 644
    }
219
220
    /**
221
     * Close curl.
222
     */
223 644
    private function close()
224
    {
225 644
        if (is_resource($this->curl)) {
226 644
            curl_close($this->curl);
227 515
        }
228 644
    }
229
230
    /* ------------------------------------------------------------------------------------------------
231
     |  Main Functions
232
     | ------------------------------------------------------------------------------------------------
233
     */
234
    /**
235
     * Get the HTTP.
236
     *
237
     * @return self
238
     */
239 1289
    public static function instance()
240
    {
241 1289
        if ( ! self::$instance) {
242 5
            self::$instance = new self;
243 4
        }
244
245 1289
        return self::$instance;
246
    }
247
248
    /**
249
     * Curl the request.
250
     *
251
     * @param  string             $method
252
     * @param  string             $url
253
     * @param  array|string|null  $params
254
     * @param  array|null         $headers
255
     * @param  bool               $hasFile
256
     *
257
     * @return array
258
     */
259 644
    public function request($method, $url, $params, $headers, $hasFile)
260
    {
261 644
        if ($method !== 'post') {
262 425
            $url    = str_parse_url($url, $params);
263 340
        }
264
        else {
265 544
            $params = $hasFile ? $params : self::encode($params);
0 ignored issues
show
Bug introduced by
It seems like $params defined by $hasFile ? $params : self::encode($params) on line 265 can also be of type null or string; however, Arcanedev\Stripe\Http\Curl\HttpClient::encode() does only seem to accept array, 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...
266
        }
267
268 644
        $this->headers->prepare($this->apiKey, $headers, $hasFile);
0 ignored issues
show
Bug introduced by
It seems like $headers defined by parameter $headers on line 259 can also be of type null; however, Arcanedev\Stripe\Http\Curl\HeaderBag::prepare() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
269 644
        $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 or null; 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...
270 644
        $this->options->setOptions([
271 644
            CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
272 644
            CURLOPT_TIMEOUT        => $this->timeout,
273 515
        ]);
274
275 644
        $this->init();
276 644
        $this->setOptionArray($this->options->get());
277 644
        $this->execute();
278
279 644
        $this->checkCertErrors();
280 644
        $this->checkResponse();
281
282 644
        $statusCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE);
283 644
        $this->close();
284
285 644
        return [$this->response, $statusCode];
286
    }
287
288
    /**
289
     * Check Cert Errors.
290
     */
291 644
    private function checkCertErrors()
292
    {
293 644
        if (SslChecker::hasCertErrors($this->errorCode)) {
294
            $this->headers->set(
295
                'X-Stripe-Client-Info',
296
                '{"ca":"using Stripe-supplied CA bundle"}'
297
            );
298
299
            $this->setOptionArray([
300
                CURLOPT_HTTPHEADER => $this->headers->get(),
301
                CURLOPT_CAINFO     => SslChecker::caBundle()
302
            ]);
303
304
            $this->execute();
305
        }
306 644
    }
307
308
    /**
309
     * Encode array to query string
310
     *
311
     * @param  array        $array
312
     * @param  string|null  $prefix
313
     *
314
     * @return string
315
     */
316 535
    protected static function encode($array, $prefix = null)
317
    {
318
        // @codeCoverageIgnoreStart
319
        if ( ! is_array($array)) return $array;
320
        // @codeCoverageIgnoreEnd
321
322 535
        $result = [];
323
324 535
        foreach ($array as $key => $value) {
325 525
            if (is_null($value)) continue;
326
327 525
            if ($prefix) {
328 435
                $key = ($key !== null && (! is_int($key) || is_array($value)))
329 435
                    ? $prefix . "[" . $key . "]"
330 435
                    : $prefix . "[]";
331 348
            }
332
333 525
            if ( ! is_array($value)) {
334 525
                $result[] = urlencode($key) . '=' . urlencode($value);
335 420
            }
336 435
            elseif ($enc = self::encode($value, $key)) {
337 453
                $result[] = $enc;
338 348
            }
339 428
        }
340
341 535
        return implode('&', $result);
342
    }
343
344
    /* ------------------------------------------------------------------------------------------------
345
     |  Other Functions
346
     | ------------------------------------------------------------------------------------------------
347
     */
348
    /**
349
     * Check Response.
350
     *
351
     * @throws \Arcanedev\Stripe\Exceptions\ApiConnectionException
352
     */
353 644
    private function checkResponse()
354
    {
355 644
        if ($this->response !== false) return;
356
357
        $this->close();
358
        $this->handleCurlError();
359
    }
360
361
    /**
362
     * Handle CURL errors.
363
     *
364
     * @throws \Arcanedev\Stripe\Exceptions\ApiConnectionException
365
     */
366
    private function handleCurlError()
367
    {
368
        switch ($this->errorCode) {
369
            case CURLE_COULDNT_CONNECT:
370
            case CURLE_COULDNT_RESOLVE_HOST:
371
            case CURLE_OPERATION_TIMEOUTED:
372
                $msg = 'Could not connect to Stripe (' . $this->apiBaseUrl . '). Please check your internet connection '
373
                    . 'and try again.  If this problem persists, you should check Stripe\'s service status at '
374
                    . 'https://twitter.com/stripestatus, or';
375
                break;
376
377
            case CURLE_SSL_CACERT:
378
            case CURLE_SSL_PEER_CERTIFICATE:
379
                $msg = 'Could not verify Stripe\'s SSL certificate.  Please make sure that your network is not '
380
                    . 'intercepting certificates. (Try going to ' . $this->apiBaseUrl . ' in your browser.) '
381
                    . 'If this problem persists,';
382
                break;
383
384
            default:
385
                $msg = 'Unexpected error communicating with Stripe. If this problem persists,';
386
                // no break
387
        }
388
389
        $msg .= ' let us know at [email protected].';
390
391
        $msg .= "\n\n(Network error [errno {$this->errorCode}]: {$this->errorMessage})";
392
393
        throw new ApiConnectionException($msg);
394
    }
395
}
396