• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stripe / stripe-php / 7065595149

01 Dec 2023 09:55PM UTC coverage: 68.923% (-0.08%) from 69.002%
7065595149

push

github

web-flow
Merge pull request #1612 from stripe/richardm-usage

Report usage of .save and StripeClient

9 of 22 new or added lines in 5 files covered. (40.91%)

1 existing line in 1 file now uncovered.

2393 of 3472 relevant lines covered (68.92%)

3.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.31
/lib/ApiRequestor.php
1
<?php
2

3
namespace Stripe;
4

5
/**
6
 * Class ApiRequestor.
7
 */
8
class ApiRequestor
9
{
10
    /**
11
     * @var null|string
12
     */
13
    private $_apiKey;
14

15
    /**
16
     * @var string
17
     */
18
    private $_apiBase;
19

20
    /**
21
     * @var HttpClient\ClientInterface
22
     */
23
    private static $_httpClient;
24
    /**
25
     * @var HttpClient\StreamingClientInterface
26
     */
27
    private static $_streamingHttpClient;
28

29
    /**
30
     * @var RequestTelemetry
31
     */
32
    private static $requestTelemetry;
33

34
    private static $OPTIONS_KEYS = ['api_key', 'idempotency_key', 'stripe_account', 'stripe_version', 'api_base'];
35

36
    /**
37
     * ApiRequestor constructor.
38
     *
39
     * @param null|string $apiKey
40
     * @param null|string $apiBase
41
     */
42
    public function __construct($apiKey = null, $apiBase = null)
20✔
43
    {
44
        $this->_apiKey = $apiKey;
20✔
45
        if (!$apiBase) {
20✔
46
            $apiBase = Stripe::$apiBase;
1✔
47
        }
48
        $this->_apiBase = $apiBase;
20✔
49
    }
50

51
    /**
52
     * Creates a telemetry json blob for use in 'X-Stripe-Client-Telemetry' headers.
53
     *
54
     * @static
55
     *
56
     * @param RequestTelemetry $requestTelemetry
57
     *
58
     * @return string
59
     */
60
    private static function _telemetryJson($requestTelemetry)
×
61
    {
62
        $payload = [
×
63
            'last_request_metrics' => [
×
64
                'request_id' => $requestTelemetry->requestId,
×
65
                'request_duration_ms' => $requestTelemetry->requestDuration,
×
66
            ],
×
67
        ];
×
NEW
68
        if (\count($requestTelemetry->usage) > 0) {
×
NEW
69
            $payload['last_request_metrics']['usage'] = $requestTelemetry->usage;
×
70
        }
71

72
        $result = \json_encode($payload);
×
73
        if (false !== $result) {
×
74
            return $result;
×
75
        }
76
        Stripe::getLogger()->error('Serializing telemetry payload failed!');
×
77

78
        return '{}';
×
79
    }
80

81
    /**
82
     * @static
83
     *
84
     * @param ApiResource|array|bool|mixed $d
85
     *
86
     * @return ApiResource|array|mixed|string
87
     */
88
    private static function _encodeObjects($d)
19✔
89
    {
90
        if ($d instanceof ApiResource) {
19✔
91
            return Util\Util::utf8($d->id);
1✔
92
        }
93
        if (true === $d) {
19✔
94
            return 'true';
1✔
95
        }
96
        if (false === $d) {
19✔
97
            return 'false';
1✔
98
        }
99
        if (\is_array($d)) {
19✔
100
            $res = [];
19✔
101
            foreach ($d as $k => $v) {
19✔
102
                $res[$k] = self::_encodeObjects($v);
1✔
103
            }
104

105
            return $res;
19✔
106
        }
107

108
        return Util\Util::utf8($d);
1✔
109
    }
110

111
    /**
112
     * @param 'delete'|'get'|'post' $method
113
     * @param string     $url
114
     * @param null|array $params
115
     * @param null|array $headers
116
     * @param string[] $usage
117
     *
118
     * @throws Exception\ApiErrorException
119
     *
120
     * @return array tuple containing (ApiReponse, API key)
121
     */
122
    public function request($method, $url, $params = null, $headers = null, $usage = [])
19✔
123
    {
124
        $params = $params ?: [];
19✔
125
        $headers = $headers ?: [];
19✔
126
        list($rbody, $rcode, $rheaders, $myApiKey) =
19✔
127
        $this->_requestRaw($method, $url, $params, $headers, $usage);
19✔
128
        $json = $this->_interpretResponse($rbody, $rcode, $rheaders);
18✔
129
        $resp = new ApiResponse($rbody, $rcode, $rheaders, $json);
4✔
130

131
        return [$resp, $myApiKey];
4✔
132
    }
133

134
    /**
135
     * @param 'delete'|'get'|'post' $method
136
     * @param string     $url
137
     * @param callable $readBodyChunkCallable
138
     * @param null|array $params
139
     * @param null|array $headers
140
     * @param string[] $usage
141
     *
142
     * @throws Exception\ApiErrorException
143
     */
NEW
144
    public function requestStream($method, $url, $readBodyChunkCallable, $params = null, $headers = null, $usage = [])
×
145
    {
146
        $params = $params ?: [];
×
147
        $headers = $headers ?: [];
×
148
        list($rbody, $rcode, $rheaders, $myApiKey) =
×
NEW
149
        $this->_requestRawStreaming($method, $url, $params, $headers, $usage, $readBodyChunkCallable);
×
150
        if ($rcode >= 300) {
×
151
            $this->_interpretResponse($rbody, $rcode, $rheaders);
×
152
        }
153
    }
154

155
    /**
156
     * @param string $rbody a JSON string
157
     * @param int $rcode
158
     * @param array $rheaders
159
     * @param array $resp
160
     *
161
     * @throws Exception\UnexpectedValueException
162
     * @throws Exception\ApiErrorException
163
     */
164
    public function handleErrorResponse($rbody, $rcode, $rheaders, $resp)
14✔
165
    {
166
        if (!\is_array($resp) || !isset($resp['error'])) {
14✔
167
            $msg = "Invalid response object from API: {$rbody} "
×
168
              . "(HTTP response code was {$rcode})";
×
169

170
            throw new Exception\UnexpectedValueException($msg);
×
171
        }
172

173
        $errorData = $resp['error'];
14✔
174

175
        $error = null;
14✔
176
        if (\is_string($errorData)) {
14✔
177
            $error = self::_specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorData);
6✔
178
        }
179
        if (!$error) {
14✔
180
            $error = self::_specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData);
8✔
181
        }
182

183
        throw $error;
14✔
184
    }
185

186
    /**
187
     * @static
188
     *
189
     * @param string $rbody
190
     * @param int    $rcode
191
     * @param array  $rheaders
192
     * @param array  $resp
193
     * @param array  $errorData
194
     *
195
     * @return Exception\ApiErrorException
196
     */
197
    private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData)
8✔
198
    {
199
        $msg = isset($errorData['message']) ? $errorData['message'] : null;
8✔
200
        $param = isset($errorData['param']) ? $errorData['param'] : null;
8✔
201
        $code = isset($errorData['code']) ? $errorData['code'] : null;
8✔
202
        $type = isset($errorData['type']) ? $errorData['type'] : null;
8✔
203
        $declineCode = isset($errorData['decline_code']) ? $errorData['decline_code'] : null;
8✔
204

205
        switch ($rcode) {
206
            case 400:
8✔
207
                // 'rate_limit' code is deprecated, but left here for backwards compatibility
208
                // for API versions earlier than 2015-09-08
209
                if ('rate_limit' === $code) {
3✔
210
                    return Exception\RateLimitException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
1✔
211
                }
212
                if ('idempotency_error' === $type) {
2✔
213
                    return Exception\IdempotencyException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
214
                }
215

216
                // no break
217
            case 404:
5✔
218
                return Exception\InvalidRequestException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
2✔
219

220
            case 401:
4✔
221
                return Exception\AuthenticationException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
222

223
            case 402:
3✔
224
                return Exception\CardException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $declineCode, $param);
1✔
225

226
            case 403:
2✔
227
                return Exception\PermissionException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
228

229
            case 429:
1✔
230
                return Exception\RateLimitException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
1✔
231

232
            default:
233
                return Exception\UnknownApiErrorException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
×
234
        }
235
    }
236

237
    /**
238
     * @static
239
     *
240
     * @param bool|string $rbody
241
     * @param int         $rcode
242
     * @param array       $rheaders
243
     * @param array       $resp
244
     * @param string      $errorCode
245
     *
246
     * @return Exception\OAuth\OAuthErrorException
247
     */
248
    private static function _specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorCode)
6✔
249
    {
250
        $description = isset($resp['error_description']) ? $resp['error_description'] : $errorCode;
6✔
251

252
        switch ($errorCode) {
253
            case 'invalid_client':
6✔
254
                return Exception\OAuth\InvalidClientException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
255

256
            case 'invalid_grant':
5✔
257
                return Exception\OAuth\InvalidGrantException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
258

259
            case 'invalid_request':
4✔
260
                return Exception\OAuth\InvalidRequestException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
261

262
            case 'invalid_scope':
3✔
263
                return Exception\OAuth\InvalidScopeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
264

265
            case 'unsupported_grant_type':
2✔
266
                return Exception\OAuth\UnsupportedGrantTypeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
267

268
            case 'unsupported_response_type':
1✔
269
                return Exception\OAuth\UnsupportedResponseTypeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
270

271
            default:
272
                return Exception\OAuth\UnknownOAuthErrorException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
×
273
        }
274
    }
275

276
    /**
277
     * @static
278
     *
279
     * @param null|array $appInfo
280
     *
281
     * @return null|string
282
     */
283
    private static function _formatAppInfo($appInfo)
19✔
284
    {
285
        if (null !== $appInfo) {
19✔
286
            $string = $appInfo['name'];
19✔
287
            if (null !== $appInfo['version']) {
19✔
288
                $string .= '/' . $appInfo['version'];
19✔
289
            }
290
            if (null !== $appInfo['url']) {
19✔
291
                $string .= ' (' . $appInfo['url'] . ')';
19✔
292
            }
293

294
            return $string;
19✔
295
        }
296

297
        return null;
×
298
    }
299

300
    /**
301
     * @static
302
     *
303
     * @param string $disableFunctionsOutput - String value of the 'disable_function' setting, as output by \ini_get('disable_functions')
304
     * @param string $functionName - Name of the function we are interesting in seeing whether or not it is disabled
305
     *
306
     * @return bool
307
     */
308
    private static function _isDisabled($disableFunctionsOutput, $functionName)
20✔
309
    {
310
        $disabledFunctions = \explode(',', $disableFunctionsOutput);
20✔
311
        foreach ($disabledFunctions as $disabledFunction) {
20✔
312
            if (\trim($disabledFunction) === $functionName) {
20✔
313
                return true;
1✔
314
            }
315
        }
316

317
        return false;
20✔
318
    }
319

320
    /**
321
     * @static
322
     *
323
     * @param string $apiKey
324
     * @param null   $clientInfo
325
     *
326
     * @return array
327
     */
328
    private static function _defaultHeaders($apiKey, $clientInfo = null)
19✔
329
    {
330
        $uaString = 'Stripe/v1 PhpBindings/' . Stripe::VERSION;
19✔
331

332
        $langVersion = \PHP_VERSION;
19✔
333
        $uname_disabled = self::_isDisabled(\ini_get('disable_functions'), 'php_uname');
19✔
334
        $uname = $uname_disabled ? '(disabled)' : \php_uname();
19✔
335

336
        $appInfo = Stripe::getAppInfo();
19✔
337
        $ua = [
19✔
338
            'bindings_version' => Stripe::VERSION,
19✔
339
            'lang' => 'php',
19✔
340
            'lang_version' => $langVersion,
19✔
341
            'publisher' => 'stripe',
19✔
342
            'uname' => $uname,
19✔
343
        ];
19✔
344
        if ($clientInfo) {
19✔
345
            $ua = \array_merge($clientInfo, $ua);
1✔
346
        }
347
        if (null !== $appInfo) {
19✔
348
            $uaString .= ' ' . self::_formatAppInfo($appInfo);
19✔
349
            $ua['application'] = $appInfo;
19✔
350
        }
351

352
        return [
19✔
353
            'X-Stripe-Client-User-Agent' => \json_encode($ua),
19✔
354
            'User-Agent' => $uaString,
19✔
355
            'Authorization' => 'Bearer ' . $apiKey,
19✔
356
            'Stripe-Version' => Stripe::getApiVersion(),
19✔
357
        ];
19✔
358
    }
359

360
    private function _prepareRequest($method, $url, $params, $headers)
19✔
361
    {
362
        $myApiKey = $this->_apiKey;
19✔
363
        if (!$myApiKey) {
19✔
364
            $myApiKey = Stripe::$apiKey;
19✔
365
        }
366

367
        if (!$myApiKey) {
19✔
368
            $msg = 'No API key provided.  (HINT: set your API key using '
1✔
369
              . '"Stripe::setApiKey(<API-KEY>)".  You can generate API keys from '
1✔
370
              . 'the Stripe web interface.  See https://stripe.com/api for '
1✔
371
              . 'details, or email support@stripe.com if you have any questions.';
1✔
372

373
            throw new Exception\AuthenticationException($msg);
1✔
374
        }
375

376
        // Clients can supply arbitrary additional keys to be included in the
377
        // X-Stripe-Client-User-Agent header via the optional getUserAgentInfo()
378
        // method
379
        $clientUAInfo = null;
18✔
380
        if (\method_exists($this->httpClient(), 'getUserAgentInfo')) {
18✔
381
            $clientUAInfo = $this->httpClient()->getUserAgentInfo();
×
382
        }
383

384
        if ($params && \is_array($params)) {
18✔
385
            $optionKeysInParams = \array_filter(
×
386
                self::$OPTIONS_KEYS,
×
387
                function ($key) use ($params) {
×
388
                    return \array_key_exists($key, $params);
×
389
                }
×
390
            );
×
391
            if (\count($optionKeysInParams) > 0) {
×
392
                $message = \sprintf('Options found in $params: %s. Options should '
×
393
                  . 'be passed in their own array after $params. (HINT: pass an '
×
394
                  . 'empty array to $params if you do not have any.)', \implode(', ', $optionKeysInParams));
×
395
                \trigger_error($message, \E_USER_WARNING);
×
396
            }
397
        }
398

399
        $absUrl = $this->_apiBase . $url;
18✔
400
        $params = self::_encodeObjects($params);
18✔
401
        $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo);
18✔
402

403
        if (Stripe::$accountId) {
18✔
404
            $defaultHeaders['Stripe-Account'] = Stripe::$accountId;
1✔
405
        }
406

407
        if (Stripe::$enableTelemetry && null !== self::$requestTelemetry) {
18✔
408
            $defaultHeaders['X-Stripe-Client-Telemetry'] = self::_telemetryJson(self::$requestTelemetry);
×
409
        }
410

411
        $hasFile = false;
18✔
412
        foreach ($params as $k => $v) {
18✔
413
            if (\is_resource($v)) {
×
414
                $hasFile = true;
×
415
                $params[$k] = self::_processResourceParam($v);
×
416
            } elseif ($v instanceof \CURLFile) {
×
417
                $hasFile = true;
×
418
            }
419
        }
420

421
        if ($hasFile) {
18✔
422
            $defaultHeaders['Content-Type'] = 'multipart/form-data';
×
423
        } else {
424
            $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
18✔
425
        }
426

427
        $combinedHeaders = \array_merge($defaultHeaders, $headers);
18✔
428
        $rawHeaders = [];
18✔
429

430
        foreach ($combinedHeaders as $header => $value) {
18✔
431
            $rawHeaders[] = $header . ': ' . $value;
18✔
432
        }
433

434
        return [$absUrl, $rawHeaders, $params, $hasFile, $myApiKey];
18✔
435
    }
436

437
    /**
438
     * @param 'delete'|'get'|'post' $method
439
     * @param string $url
440
     * @param array $params
441
     * @param array $headers
442
     * @param string[] $usage
443
     *
444
     * @throws Exception\AuthenticationException
445
     * @throws Exception\ApiConnectionException
446
     *
447
     * @return array
448
     */
449
    private function _requestRaw($method, $url, $params, $headers, $usage)
19✔
450
    {
451
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers);
19✔
452

453
        $requestStartMs = Util\Util::currentTimeMillis();
18✔
454

455
        list($rbody, $rcode, $rheaders) = $this->httpClient()->request(
18✔
456
            $method,
18✔
457
            $absUrl,
18✔
458
            $rawHeaders,
18✔
459
            $params,
18✔
460
            $hasFile
18✔
461
        );
18✔
462

463
        if (isset($rheaders['request-id'])
18✔
464
        && \is_string($rheaders['request-id'])
18✔
465
        && '' !== $rheaders['request-id']) {
18✔
466
            self::$requestTelemetry = new RequestTelemetry(
×
467
                $rheaders['request-id'],
×
NEW
468
                Util\Util::currentTimeMillis() - $requestStartMs,
×
NEW
469
                $usage
×
UNCOV
470
            );
×
471
        }
472

473
        return [$rbody, $rcode, $rheaders, $myApiKey];
18✔
474
    }
475

476
    /**
477
     * @param 'delete'|'get'|'post' $method
478
     * @param string $url
479
     * @param array $params
480
     * @param array $headers
481
     * @param string[] $usage
482
     * @param callable $readBodyChunkCallable
483
     *
484
     * @throws Exception\AuthenticationException
485
     * @throws Exception\ApiConnectionException
486
     *
487
     * @return array
488
     */
NEW
489
    private function _requestRawStreaming($method, $url, $params, $headers, $usage, $readBodyChunkCallable)
×
490
    {
491
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers);
×
492

493
        $requestStartMs = Util\Util::currentTimeMillis();
×
494

495
        list($rbody, $rcode, $rheaders) = $this->streamingHttpClient()->requestStream(
×
496
            $method,
×
497
            $absUrl,
×
498
            $rawHeaders,
×
499
            $params,
×
500
            $hasFile,
×
501
            $readBodyChunkCallable
×
502
        );
×
503

504
        if (isset($rheaders['request-id'])
×
505
        && \is_string($rheaders['request-id'])
×
506
        && '' !== $rheaders['request-id']) {
×
507
            self::$requestTelemetry = new RequestTelemetry(
×
508
                $rheaders['request-id'],
×
509
                Util\Util::currentTimeMillis() - $requestStartMs
×
510
            );
×
511
        }
512

513
        return [$rbody, $rcode, $rheaders, $myApiKey];
×
514
    }
515

516
    /**
517
     * @param resource $resource
518
     *
519
     * @throws Exception\InvalidArgumentException
520
     *
521
     * @return \CURLFile|string
522
     */
523
    private function _processResourceParam($resource)
×
524
    {
525
        if ('stream' !== \get_resource_type($resource)) {
×
526
            throw new Exception\InvalidArgumentException(
×
527
                'Attempted to upload a resource that is not a stream'
×
528
            );
×
529
        }
530

531
        $metaData = \stream_get_meta_data($resource);
×
532
        if ('plainfile' !== $metaData['wrapper_type']) {
×
533
            throw new Exception\InvalidArgumentException(
×
534
                'Only plainfile resource streams are supported'
×
535
            );
×
536
        }
537

538
        // We don't have the filename or mimetype, but the API doesn't care
539
        return new \CURLFile($metaData['uri']);
×
540
    }
541

542
    /**
543
     * @param string $rbody
544
     * @param int    $rcode
545
     * @param array  $rheaders
546
     *
547
     * @throws Exception\UnexpectedValueException
548
     * @throws Exception\ApiErrorException
549
     *
550
     * @return array
551
     */
552
    private function _interpretResponse($rbody, $rcode, $rheaders)
18✔
553
    {
554
        $resp = \json_decode($rbody, true);
18✔
555
        $jsonError = \json_last_error();
18✔
556
        if (null === $resp && \JSON_ERROR_NONE !== $jsonError) {
18✔
557
            $msg = "Invalid response body from API: {$rbody} "
×
558
              . "(HTTP response code was {$rcode}, json_last_error() was {$jsonError})";
×
559

560
            throw new Exception\UnexpectedValueException($msg, $rcode);
×
561
        }
562

563
        if ($rcode < 200 || $rcode >= 300) {
18✔
564
            $this->handleErrorResponse($rbody, $rcode, $rheaders, $resp);
14✔
565
        }
566

567
        return $resp;
4✔
568
    }
569

570
    /**
571
     * @static
572
     *
573
     * @param HttpClient\ClientInterface $client
574
     */
575
    public static function setHttpClient($client)
23✔
576
    {
577
        self::$_httpClient = $client;
23✔
578
    }
579

580
    /**
581
     * @static
582
     *
583
     * @param HttpClient\StreamingClientInterface $client
584
     */
585
    public static function setStreamingHttpClient($client)
23✔
586
    {
587
        self::$_streamingHttpClient = $client;
23✔
588
    }
589

590
    /**
591
     * @static
592
     *
593
     * Resets any stateful telemetry data
594
     */
595
    public static function resetTelemetry()
×
596
    {
597
        self::$requestTelemetry = null;
×
598
    }
599

600
    /**
601
     * @return HttpClient\ClientInterface
602
     */
603
    private function httpClient()
19✔
604
    {
605
        if (!self::$_httpClient) {
19✔
606
            self::$_httpClient = HttpClient\CurlClient::instance();
×
607
        }
608

609
        return self::$_httpClient;
19✔
610
    }
611

612
    /**
613
     * @return HttpClient\StreamingClientInterface
614
     */
615
    private function streamingHttpClient()
×
616
    {
617
        if (!self::$_streamingHttpClient) {
×
618
            self::$_streamingHttpClient = HttpClient\CurlClient::instance();
×
619
        }
620

621
        return self::$_streamingHttpClient;
×
622
    }
623
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc