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

jwilsson / spotify-web-api-php / 20000597411

07 Dec 2025 07:02AM UTC coverage: 97.399% (-0.01%) from 97.413%
20000597411

push

github

jwilsson
Replace test mocks with stubs

749 of 769 relevant lines covered (97.4%)

16.35 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace SpotifyWebAPI;
6

7
class Request
8
{
9
    public const string ACCOUNT_URL = 'https://accounts.spotify.com';
10
    public const string 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);
105✔
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);
60✔
40

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

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

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

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

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

105
        array_shift($headers);
57✔
106

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

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

115
        return $parsedHeaders;
57✔
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);
6✔
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);
15✔
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;
3✔
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 = [];
60✔
198

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

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

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

221
                break;
9✔
222
            case 'POST':
51✔
223
                $options[CURLOPT_POST] = true;
9✔
224
                $options[CURLOPT_POSTFIELDS] = $parameters;
9✔
225

226
                break;
9✔
227
            default:
228
                $options[CURLOPT_CUSTOMREQUEST] = $method;
42✔
229

230
                if ($parameters) {
42✔
231
                    $options[CURLOPT_URL] .= '/?' . $parameters;
6✔
232
                }
233

234
                break;
42✔
235
        }
236

237
        $ch = curl_init();
60✔
238

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

241
        $response = curl_exec($ch);
60✔
242

243
        if (curl_error($ch)) {
60✔
244
            $error = curl_error($ch);
3✔
245
            $errno = curl_errno($ch);
3✔
246

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

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

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

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

263
        if ($status >= 400) {
57✔
264
            $this->handleResponseError($body, $status);
21✔
265
        }
266

267
        return $this->lastResponse;
36✔
268
    }
269

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

281
        return $this;
105✔
282
    }
283

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

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

304
        return [
57✔
305
            $parts[0],
57✔
306
            $parts[1],
57✔
307
        ];
57✔
308
    }
309
}
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