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

jwilsson / spotify-web-api-php / 19985059477

06 Dec 2025 07:07AM UTC coverage: 98.747% (-0.003%) from 98.75%
19985059477

push

github

jwilsson
6.3.1 changelog

788 of 798 relevant lines covered (98.75%)

27.22 hits per line

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

95.56
/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
        'default_headers' => [],
16
        'return_assoc' => false,
17
    ];
18

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

30
    /**
31
     * Format request headers to be used with cURL.
32
     *
33
     * @param array|object $headers The headers to format.
34
     *
35
     * @return array The formatted headers.
36
     */
37
    protected function formatHeaders(array|object $headers): array
38
    {
39
        $headers = array_merge((array) $this->options['default_headers'], $headers);
100✔
40

41
        $formattedHeaders = [];
100✔
42
        foreach ($headers as $key => $val) {
100✔
43
            $formattedHeaders[] = "$key: $val";
15✔
44
        }
45

46
        return $formattedHeaders;
100✔
47
    }
48

49
    /**
50
     * Handle response errors.
51
     *
52
     * @param string $body The raw, unparsed response body.
53
     * @param int $status The HTTP status code, passed along to any exceptions thrown.
54
     *
55
     * @throws SpotifyWebAPIException
56
     * @throws SpotifyWebAPIAuthException
57
     *
58
     * @return void
59
     */
60
    protected function handleResponseError(string $body, int $status): void
61
    {
62
        $parsedBody = json_decode($body);
35✔
63
        $error = $parsedBody->error ?? null;
35✔
64

65
        if (isset($error->message) && isset($error->status)) {
35✔
66
            // It's an API call error
67
            $exception = new SpotifyWebAPIException($error->message, $error->status);
10✔
68

69
            if (isset($error->reason)) {
10✔
70
                $exception->setReason($error->reason);
5✔
71
            }
72

73
            throw $exception;
10✔
74
        } elseif (isset($parsedBody->error_description)) {
25✔
75
            // It's an auth call error
76
            throw new SpotifyWebAPIAuthException($parsedBody->error_description, $status);
15✔
77
        } elseif ($body) {
10✔
78
            // Something else went wrong, try to give at least some info
79
            throw new SpotifyWebAPIException($body, $status);
5✔
80
        } else {
81
            // Something went really wrong, we don't know what
82
            throw new SpotifyWebAPIException('An unknown error occurred.', $status);
5✔
83
        }
84
    }
85

86
    /**
87
     * Parse HTTP response body, taking the "return_assoc" option into account.
88
     */
89
    protected function parseBody(string $body): mixed
90
    {
91
        return json_decode($body, $this->options['return_assoc']);
95✔
92
    }
93

94
    /**
95
     * Parse HTTP response headers and normalize names.
96
     *
97
     * @param string $headers The raw, unparsed response headers.
98
     *
99
     * @return array Headers as key–value pairs.
100
     */
101
    protected function parseHeaders(string $headers): array
102
    {
103
        $headers = explode("\n", $headers);
95✔
104

105
        array_shift($headers);
95✔
106

107
        $parsedHeaders = [];
95✔
108
        foreach ($headers as $header) {
95✔
109
            [$key, $value] = explode(':', $header, 2);
95✔
110

111
            $key = strtolower($key);
95✔
112
            $parsedHeaders[$key] = trim($value);
95✔
113
        }
114

115
        return $parsedHeaders;
95✔
116
    }
117

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

140
    /**
141
     * Make a request to the "api" endpoint.
142
     *
143
     * @param string $method The HTTP method to use.
144
     * @param string $uri The URI to request.
145
     * @param string|array $parameters Optional. Query string parameters or HTTP body, depending on $method.
146
     * @param array $headers Optional. HTTP headers.
147
     *
148
     * @throws SpotifyWebAPIException
149
     * @throws SpotifyWebAPIAuthException
150
     *
151
     * @return array Response data.
152
     * - array|object body The response body. Type is controlled by the `return_assoc` option.
153
     * - array headers Response headers.
154
     * - int status HTTP status code.
155
     * - string url The requested URL.
156
     */
157
    public function api(string $method, string $uri, string|array $parameters = [], array $headers = []): array
158
    {
159
        return $this->send($method, static::API_URL . $uri, $parameters, $headers);
25✔
160
    }
161

162
    /**
163
     * Get the latest full response from the Spotify API.
164
     *
165
     * @return array Response data.
166
     * - array|object body The response body. Type is controlled by the `return_assoc` option.
167
     * - array headers Response headers.
168
     * - int status HTTP status code.
169
     * - string url The requested URL.
170
     */
171
    public function getLastResponse(): array
172
    {
173
        return $this->lastResponse;
5✔
174
    }
175

176
    /**
177
     * Make a request to Spotify.
178
     * You'll probably want to use one of the convenience methods instead.
179
     *
180
     * @param string $method The HTTP method to use.
181
     * @param string $url The URL to request.
182
     * @param string|array|object $parameters Optional. Query string parameters or HTTP body, depending on $method.
183
     * @param array $headers Optional. HTTP headers.
184
     *
185
     * @throws SpotifyWebAPIException
186
     * @throws SpotifyWebAPIAuthException
187
     *
188
     * @return array Response data.
189
     * - array|object body The response body. Type is controlled by the `return_assoc` option.
190
     * - array headers Response headers.
191
     * - int status HTTP status code.
192
     * - string url The requested URL.
193
     */
194
    public function send(string $method, string $url, string|array|object $parameters = [], array $headers = []): array
195
    {
196
        // Reset any old responses
197
        $this->lastResponse = [];
100✔
198

199
        // Sometimes a stringified JSON object is passed
200
        if (is_array($parameters) || is_object($parameters)) {
100✔
201
            $parameters = http_build_query($parameters, '', '&');
100✔
202
        }
203

204
        $method = strtoupper($method);
100✔
205
        $options = [
100✔
206
            CURLOPT_CAINFO => __DIR__ . '/cacert.pem',
100✔
207
            CURLOPT_ENCODING => '',
100✔
208
            CURLOPT_HEADER => true,
100✔
209
            CURLOPT_HTTPHEADER => $this->formatHeaders($headers),
100✔
210
            CURLOPT_RETURNTRANSFER => true,
100✔
211
            CURLOPT_URL => rtrim($url, '/'),
100✔
212
        ];
100✔
213

214
        switch ($method) {
215
            case 'DELETE': // No break
100✔
216
            case 'PUT':
95✔
217
                $options[CURLOPT_CUSTOMREQUEST] = $method;
15✔
218
                $options[CURLOPT_POSTFIELDS] = $parameters;
15✔
219

220
                break;
15✔
221
            case 'POST':
85✔
222
                $options[CURLOPT_POST] = true;
15✔
223
                $options[CURLOPT_POSTFIELDS] = $parameters;
15✔
224

225
                break;
15✔
226
            default:
227
                $options[CURLOPT_CUSTOMREQUEST] = $method;
70✔
228

229
                if ($parameters) {
70✔
230
                    $options[CURLOPT_URL] .= '/?' . $parameters;
10✔
231
                }
232

233
                break;
70✔
234
        }
235

236
        $ch = curl_init();
100✔
237

238
        curl_setopt_array($ch, array_replace($options, $this->options['curl_options']));
100✔
239

240
        $response = curl_exec($ch);
100✔
241

242
        if (curl_error($ch)) {
100✔
243
            $error = curl_error($ch);
5✔
244
            $errno = curl_errno($ch);
5✔
245

246
            throw new SpotifyWebAPIException('cURL transport error: ' . $errno . ' ' . $error);
5✔
247
        }
248

249
        [$headers, $body] = $this->splitResponse($response);
95✔
250

251
        $parsedBody = $this->parseBody($body);
95✔
252
        $parsedHeaders = $this->parseHeaders($headers);
95✔
253
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
95✔
254

255
        $this->lastResponse = [
95✔
256
            'body' => $parsedBody,
95✔
257
            'headers' => $parsedHeaders,
95✔
258
            'status' => $status,
95✔
259
            'url' => $url,
95✔
260
        ];
95✔
261

262
        if ($status >= 400) {
95✔
263
            $this->handleResponseError($body, $status);
35✔
264
        }
265

266
        return $this->lastResponse;
60✔
267
    }
268

269
    /**
270
     * Set options
271
     *
272
     * @param array|object $options Options to set.
273
     *
274
     * @return self
275
     */
276
    public function setOptions(array|object $options): self
277
    {
278
        $this->options = array_merge($this->options, (array) $options);
175✔
279

280
        return $this;
175✔
281
    }
282

283
    /**
284
     * Split response into headers and body, taking proxy response headers etc. into account.
285
     *
286
     * @param string $response The complete response.
287
     *
288
     * @return array An array consisting of two elements, headers and body.
289
     */
290
    protected function splitResponse(string $response): array
291
    {
292
        $response = str_replace("\r\n", "\n", $response);
95✔
293
        $parts = explode("\n\n", $response, 3);
95✔
294

295
        // Skip first set of headers for proxied requests etc.
296
        if (
297
            preg_match('/^HTTP\/1.\d 100 Continue/', $parts[0]) ||
95✔
298
            preg_match('/^HTTP\/1.\d 200 Connection established/', $parts[0]) ||
95✔
299
            preg_match('/^HTTP\/1.\d 200 Tunnel established/', $parts[0])
95✔
300
        ) {
301
            return [
×
302
                $parts[1],
×
303
                $parts[2],
×
304
            ];
×
305
        }
306

307
        return [
95✔
308
            $parts[0],
95✔
309
            $parts[1],
95✔
310
        ];
95✔
311
    }
312
}
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