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

jwilsson / spotify-web-api-php / 9400189815

06 Jun 2024 11:37AM UTC coverage: 98.674% (+0.002%) from 98.672%
9400189815

push

github

jwilsson
6.1.0 changelog

744 of 754 relevant lines covered (98.67%)

16.27 hits per line

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

95.51
/src/Request.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace SpotifyWebAPI;
6

7
class Request
8
{
9
    public const ACCOUNT_URL = 'https://accounts.spotify.com';
10
    public const API_URL = 'https://api.spotify.com';
11

12
    protected array $lastResponse = [];
13
    protected array $options = [
14
        'curl_options' => [],
15
        'return_assoc' => false,
16
    ];
17

18
    /**
19
     * Constructor
20
     * Set options.
21
     *
22
     * @param array|object $options Optional. Options to set.
23
     */
24
    public function __construct(array|object $options = [])
25
    {
26
        $this->setOptions($options);
105✔
27
    }
28

29
    /**
30
     * Handle response errors.
31
     *
32
     * @param string $body The raw, unparsed response body.
33
     * @param int $status The HTTP status code, passed along to any exceptions thrown.
34
     *
35
     * @throws SpotifyWebAPIException
36
     * @throws SpotifyWebAPIAuthException
37
     *
38
     * @return void
39
     */
40
    protected function handleResponseError(string $body, int $status): void
41
    {
42
        $parsedBody = json_decode($body);
21✔
43
        $error = $parsedBody->error ?? null;
21✔
44

45
        if (isset($error->message) && isset($error->status)) {
21✔
46
            // It's an API call error
47
            $exception = new SpotifyWebAPIException($error->message, $error->status);
6✔
48

49
            if (isset($error->reason)) {
6✔
50
                $exception->setReason($error->reason);
3✔
51
            }
52

53
            throw $exception;
6✔
54
        } elseif (isset($parsedBody->error_description)) {
15✔
55
            // It's an auth call error
56
            throw new SpotifyWebAPIAuthException($parsedBody->error_description, $status);
9✔
57
        } elseif ($body) {
6✔
58
            // Something else went wrong, try to give at least some info
59
            throw new SpotifyWebAPIException($body, $status);
3✔
60
        } else {
61
            // Something went really wrong, we don't know what
62
            throw new SpotifyWebAPIException('An unknown error occurred.', $status);
3✔
63
        }
64
    }
65

66
    /**
67
     * Parse HTTP response body, taking the "return_assoc" option into account.
68
     */
69
    protected function parseBody(string $body): mixed
70
    {
71
        return json_decode($body, $this->options['return_assoc']);
57✔
72
    }
73

74
    /**
75
     * Parse HTTP response headers and normalize names.
76
     *
77
     * @param string $headers The raw, unparsed response headers.
78
     *
79
     * @return array Headers as key–value pairs.
80
     */
81
    protected function parseHeaders(string $headers): array
82
    {
83
        $headers = explode("\n", $headers);
57✔
84

85
        array_shift($headers);
57✔
86

87
        $parsedHeaders = [];
57✔
88
        foreach ($headers as $header) {
57✔
89
            [$key, $value] = explode(':', $header, 2);
57✔
90

91
            $key = strtolower($key);
57✔
92
            $parsedHeaders[$key] = trim($value);
57✔
93
        }
94

95
        return $parsedHeaders;
57✔
96
    }
97

98
    /**
99
     * Make a request to the "account" endpoint.
100
     *
101
     * @param string $method The HTTP method to use.
102
     * @param string $uri The URI to request.
103
     * @param string|array $parameters Optional. Query string parameters or HTTP body, depending on $method.
104
     * @param array $headers Optional. HTTP headers.
105
     *
106
     * @throws SpotifyWebAPIException
107
     * @throws SpotifyWebAPIAuthException
108
     *
109
     * @return array Response data.
110
     * - array|object body The response body. Type is controlled by the `return_assoc` option.
111
     * - array headers Response headers.
112
     * - int status HTTP status code.
113
     * - string url The requested URL.
114
     */
115
    public function account(string $method, string $uri, string|array $parameters = [], array $headers = []): array
116
    {
117
        return $this->send($method, self::ACCOUNT_URL . $uri, $parameters, $headers);
6✔
118
    }
119

120
    /**
121
     * Make a request to the "api" endpoint.
122
     *
123
     * @param string $method The HTTP method to use.
124
     * @param string $uri The URI to request.
125
     * @param string|array $parameters Optional. Query string parameters or HTTP body, depending on $method.
126
     * @param array $headers Optional. HTTP headers.
127
     *
128
     * @throws SpotifyWebAPIException
129
     * @throws SpotifyWebAPIAuthException
130
     *
131
     * @return array Response data.
132
     * - array|object body The response body. Type is controlled by the `return_assoc` option.
133
     * - array headers Response headers.
134
     * - int status HTTP status code.
135
     * - string url The requested URL.
136
     */
137
    public function api(string $method, string $uri, string|array $parameters = [], array $headers = []): array
138
    {
139
        return $this->send($method, self::API_URL . $uri, $parameters, $headers);
15✔
140
    }
141

142
    /**
143
     * Get the latest full response from the Spotify API.
144
     *
145
     * @return array Response data.
146
     * - array|object body The response body. Type is controlled by the `return_assoc` option.
147
     * - array headers Response headers.
148
     * - int status HTTP status code.
149
     * - string url The requested URL.
150
     */
151
    public function getLastResponse(): array
152
    {
153
        return $this->lastResponse;
3✔
154
    }
155

156
    /**
157
     * Make a request to Spotify.
158
     * You'll probably want to use one of the convenience methods instead.
159
     *
160
     * @param string $method The HTTP method to use.
161
     * @param string $url The URL to request.
162
     * @param string|array|object $parameters Optional. Query string parameters or HTTP body, depending on $method.
163
     * @param array $headers Optional. HTTP headers.
164
     *
165
     * @throws SpotifyWebAPIException
166
     * @throws SpotifyWebAPIAuthException
167
     *
168
     * @return array Response data.
169
     * - array|object body The response body. Type is controlled by the `return_assoc` option.
170
     * - array headers Response headers.
171
     * - int status HTTP status code.
172
     * - string url The requested URL.
173
     */
174
    public function send(string $method, string $url, string|array|object $parameters = [], array $headers = []): array
175
    {
176
        // Reset any old responses
177
        $this->lastResponse = [];
60✔
178

179
        // Sometimes a stringified JSON object is passed
180
        if (is_array($parameters) || is_object($parameters)) {
60✔
181
            $parameters = http_build_query($parameters, '', '&');
60✔
182
        }
183

184
        $options = [
60✔
185
            CURLOPT_CAINFO => __DIR__ . '/cacert.pem',
60✔
186
            CURLOPT_ENCODING => '',
60✔
187
            CURLOPT_HEADER => true,
60✔
188
            CURLOPT_HTTPHEADER => [],
60✔
189
            CURLOPT_RETURNTRANSFER => true,
60✔
190
            CURLOPT_URL => rtrim($url, '/'),
60✔
191
        ];
60✔
192

193
        foreach ($headers as $key => $val) {
60✔
194
            $options[CURLOPT_HTTPHEADER][] = "$key: $val";
9✔
195
        }
196

197
        $method = strtoupper($method);
60✔
198

199
        switch ($method) {
200
            case 'DELETE': // No break
60✔
201
            case 'PUT':
57✔
202
                $options[CURLOPT_CUSTOMREQUEST] = $method;
9✔
203
                $options[CURLOPT_POSTFIELDS] = $parameters;
9✔
204

205
                break;
9✔
206
            case 'POST':
51✔
207
                $options[CURLOPT_POST] = true;
9✔
208
                $options[CURLOPT_POSTFIELDS] = $parameters;
9✔
209

210
                break;
9✔
211
            default:
212
                $options[CURLOPT_CUSTOMREQUEST] = $method;
42✔
213

214
                if ($parameters) {
42✔
215
                    $options[CURLOPT_URL] .= '/?' . $parameters;
6✔
216
                }
217

218
                break;
42✔
219
        }
220

221
        $ch = curl_init();
60✔
222

223
        curl_setopt_array($ch, array_replace($options, $this->options['curl_options']));
60✔
224

225
        $response = curl_exec($ch);
60✔
226

227
        if (curl_error($ch)) {
60✔
228
            $error = curl_error($ch);
3✔
229
            $errno = curl_errno($ch);
3✔
230
            curl_close($ch);
3✔
231

232
            throw new SpotifyWebAPIException('cURL transport error: ' . $errno . ' ' . $error);
3✔
233
        }
234

235
        [$headers, $body] = $this->splitResponse($response);
57✔
236

237
        $parsedBody = $this->parseBody($body);
57✔
238
        $parsedHeaders = $this->parseHeaders($headers);
57✔
239
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
57✔
240

241
        $this->lastResponse = [
57✔
242
            'body' => $parsedBody,
57✔
243
            'headers' => $parsedHeaders,
57✔
244
            'status' => $status,
57✔
245
            'url' => $url,
57✔
246
        ];
57✔
247

248
        curl_close($ch);
57✔
249

250
        if ($status >= 400) {
57✔
251
            $this->handleResponseError($body, $status);
21✔
252
        }
253

254
        return $this->lastResponse;
36✔
255
    }
256

257
    /**
258
     * Set options
259
     *
260
     * @param array|object $options Options to set.
261
     *
262
     * @return self
263
     */
264
    public function setOptions(array|object $options): self
265
    {
266
        $this->options = array_merge($this->options, (array) $options);
105✔
267

268
        return $this;
105✔
269
    }
270

271
    /**
272
     * Split response into headers and body, taking proxy response headers etc. into account.
273
     *
274
     * @param string $response The complete response.
275
     *
276
     * @return array An array consisting of two elements, headers and body.
277
     */
278
    protected function splitResponse(string $response): array
279
    {
280
        $response = str_replace("\r\n", "\n", $response);
57✔
281
        $parts = explode("\n\n", $response, 3);
57✔
282

283
        // Skip first set of headers for proxied requests etc.
284
        if (
285
            preg_match('/^HTTP\/1.\d 100 Continue/', $parts[0]) ||
57✔
286
            preg_match('/^HTTP\/1.\d 200 Connection established/', $parts[0]) ||
57✔
287
            preg_match('/^HTTP\/1.\d 200 Tunnel established/', $parts[0])
57✔
288
        ) {
289
            return [
×
290
                $parts[1],
×
291
                $parts[2],
×
292
            ];
×
293
        }
294

295
        return [
57✔
296
            $parts[0],
57✔
297
            $parts[1],
57✔
298
        ];
57✔
299
    }
300
}
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