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

jwilsson / spotify-web-api-php / 18440423135

12 Oct 2025 06:36AM UTC coverage: 98.75% (+0.05%) from 98.701%
18440423135

push

github

jwilsson
Add support for timestamped_ids to SpotifyWebAPI::addMyTracks()

23 of 23 new or added lines in 1 file covered. (100.0%)

7 existing lines in 2 files now uncovered.

790 of 800 relevant lines covered (98.75%)

21.83 hits per line

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

95.65
/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);
140✔
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);
80✔
40

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

46
        return $formattedHeaders;
80✔
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);
28✔
63
        $error = $parsedBody->error ?? null;
28✔
64

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

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

73
            throw $exception;
8✔
74
        } elseif (isset($parsedBody->error_description)) {
20✔
75
            // It's an auth call error
76
            throw new SpotifyWebAPIAuthException($parsedBody->error_description, $status);
12✔
77
        } elseif ($body) {
8✔
78
            // Something else went wrong, try to give at least some info
79
            throw new SpotifyWebAPIException($body, $status);
4✔
80
        } else {
81
            // Something went really wrong, we don't know what
82
            throw new SpotifyWebAPIException('An unknown error occurred.', $status);
4✔
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']);
76✔
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);
76✔
104

105
        array_shift($headers);
76✔
106

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

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

115
        return $parsedHeaders;
76✔
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);
8✔
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);
20✔
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;
4✔
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 = [];
80✔
198

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

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

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

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

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

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

233
                break;
56✔
234
        }
235

236
        $ch = curl_init();
80✔
237

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

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

242
        if (curl_error($ch)) {
80✔
243
            $error = curl_error($ch);
4✔
244
            $errno = curl_errno($ch);
4✔
245
            curl_close($ch);
4✔
246

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

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

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

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

263
        curl_close($ch);
76✔
264

265
        if ($status >= 400) {
76✔
266
            $this->handleResponseError($body, $status);
28✔
267
        }
268

269
        return $this->lastResponse;
48✔
270
    }
271

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

283
        return $this;
140✔
284
    }
285

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

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

310
        return [
76✔
311
            $parts[0],
76✔
312
            $parts[1],
76✔
313
        ];
76✔
314
    }
315
}
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