Passed
Push — main ( 3b5d63...9f2139 )
by Thomas
13:38
created

Request::headers()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 12
ccs 8
cts 8
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Chuck;
6
7
use Conia\Chuck\Exception\OutOfBoundsException;
8
use Conia\Chuck\Exception\RuntimeException;
9
use Conia\Chuck\Http\WrapsMessage;
10
use Conia\Chuck\Http\WrapsRequest;
11
use Psr\Http\Message\ServerRequestInterface as PsrServerRequest;
12
use Psr\Http\Message\StreamInterface as PsrStream;
13
use Psr\Http\Message\UploadedFileInterface as PsrUploadedFile;
14
use Psr\Http\Message\UriInterface as PsrUri;
15
use Throwable;
16
17
/** @psalm-api */
18
class Request
19
{
20
    use WrapsMessage;
21
    use WrapsRequest;
22
23 185
    public function __construct(protected PsrServerRequest $psr)
24
    {
25 185
    }
26
27 2
    public function psr(): PsrServerRequest
28
    {
29 2
        return $this->psr;
30
    }
31
32 1
    public function setPsr(PsrServerRequest $psr): static
33
    {
34 1
        $this->psr = $psr;
35
36 1
        return $this;
37
    }
38
39 1
    public function params(): array
40
    {
41 1
        return $this->psr->getQueryParams();
42
    }
43
44 3
    public function param(string $key, mixed $default = null): mixed
45
    {
46 3
        $params = $this->psr->getQueryParams();
47 3
        $error = 'Query string variable not found';
48
49 3
        return $this->returnOrFail($params, $key, $default, $error, func_num_args());
50
    }
51
52 1
    public function form(): ?array
53
    {
54 1
        $body = $this->psr->getParsedBody();
55 1
        assert(is_null($body) || is_array($body));
56
57 1
        return $body;
58
    }
59
60 4
    public function field(string $key, mixed $default = null): mixed
61
    {
62 4
        $body = $this->psr->getParsedBody();
63 4
        assert(is_null($body) || is_array($body));
64 4
        $error = 'Form field not found';
65
66 4
        return $this->returnOrFail($body, $key, $default, $error, func_num_args());
67
    }
68
69 1
    public function cookies(): array
70
    {
71 1
        return $this->psr->getCookieParams();
72
    }
73
74 3
    public function cookie(string $key, mixed $default = null): mixed
75
    {
76 3
        $params = $this->psr->getCookieParams();
77 3
        $error = 'Cookie not found';
78
79 3
        return $this->returnOrFail($params, $key, $default, $error, func_num_args());
80
    }
81
82 1
    public function serverParams(): array
83
    {
84 1
        return $this->psr->getServerParams();
85
    }
86
87 3
    public function server(string $key, mixed $default = null): mixed
88
    {
89 3
        $params = $this->psr->getServerParams();
90 3
        $error = 'Server parameter not found';
91
92 3
        return $this->returnOrFail($params, $key, $default, $error, func_num_args());
93
    }
94
95 1
    public function header(string $name): string
96
    {
97 1
        return $this->psr->getHeaderLine($name);
98
    }
99
100 2
    public function headers(bool $firstOnly = false): array
101
    {
102 2
        $headers = $this->psr->getHeaders();
103
104 2
        if ($firstOnly) {
105 1
            return array_combine(
106 1
                array_keys($headers),
107 1
                array_map(fn (array $v): string => $v[0], $headers),
108 1
            );
109
        }
110
111 1
        return $headers;
112
    }
113
114 9
    public function accept(): array
115
    {
116 9
        return explode(',', $this->getHeaderLine('Accept'));
117
    }
118
119 1
    public function attributes(): array
120
    {
121 1
        return $this->psr->getAttributes();
122
    }
123
124 1
    public function set(string $attribute, mixed $value): static
125
    {
126 1
        $this->psr = $this->psr->withAttribute($attribute, $value);
127
128 1
        return $this;
129
    }
130
131 3
    public function get(string $key, mixed $default = null): mixed
132
    {
133 3
        $params = $this->psr->getAttributes();
134 3
        $error = 'Request attribute not found';
135
136 3
        return $this->returnOrFail($params, $key, $default, $error, func_num_args());
137
    }
138
139 62
    public function uri(): PsrUri
140
    {
141 62
        return $this->psr->getUri();
142
    }
143
144 1
    public function origin(): string
145
    {
146 1
        $uri = $this->psr->getUri();
147 1
        $scheme = $uri->getScheme();
148 1
        $origin = $scheme ? $scheme . ':' : '';
149 1
        $authority = $uri->getAuthority();
150 1
        $origin .= $authority ? '//' . $authority : '';
151
152 1
        return $origin;
153
    }
154
155 62
    public function method(): string
156
    {
157 62
        return strtoupper($this->psr->getMethod());
158
    }
159
160 1
    public function isMethod(string $method): bool
161
    {
162 1
        return strtoupper($method) === $this->method();
163
    }
164
165 1
    public function body(): PsrStream
166
    {
167 1
        return $this->psr->getBody();
168
    }
169
170 2
    public function json(
171
        int $flags = JSON_OBJECT_AS_ARRAY,
172
    ): mixed {
173 2
        $body = (string)$this->psr->getBody();
174
175 2
        return json_decode(
176 2
            $body,
177 2
            true,
178 2
            512, // PHP default value
179 2
            $flags,
180 2
        );
181
    }
182
183
    /**
184
     * Returns always a list of uploaded files, even if there is
185
     * only one file.
186
     *
187
     * Psalm does not support multi file uploads yet and complains
188
     * about type issues. We need to suppres some of the errors.
189
     *
190
     * @no-named-arguments
191
     *
192
     * @psalm-param list<string>|string ...$keys
193
     *
194
     * @throws OutOfBoundsException RuntimeException
195
     */
196 8
    public function files(array|string ...$keys): array
197
    {
198 8
        $files = $this->psr->getUploadedFiles();
199 8
        $keys = $this->validateKeys($keys);
200
201 7
        if (count($keys) === 0) {
202 1
            return $files;
203
        }
204
205
        try {
206 6
            foreach ($keys as $key) {
207
                /**
208
                 * @psalm-suppress MixedAssignment, MixedArrayAccess
209
                 *
210
                 * Psalm does not support recursive types like:
211
                 *     T = array<string, string|T>
212
                 */
213 6
                $files = $files[$key];
214
            }
215 2
        } catch (Throwable) {
216 2
            throw new OutOfBoundsException('Invalid files key ' . $this->formatKeys($keys));
217
        }
218
219 4
        if ($files instanceof PsrUploadedFile) {
220 1
            return [$files];
221
        }
222
223 3
        assert(is_array($files));
224
225 3
        return $files;
226
    }
227
228
    /**
229
     * Returns always a list of uploaded files, even if there is
230
     * only one file.
231
     *
232
     * Psalm does not support multi file uploads yet and complains
233
     * about type issues. We need to suppres some of the errors.
234
     *
235
     * @no-named-arguments
236
     *
237
     * @psalm-param list<non-empty-string>|string ...$keys
238
     *
239
     * @return PsrUploadedFile
240
     *
241
     * @throws OutOfBoundsException RuntimeException
242
     */
243 7
    public function file(array|string ...$keys): PsrUploadedFile|array
244
    {
245 7
        $keys = $this->validateKeys($keys);
246
247 7
        if (count($keys) === 0) {
248 1
            throw new RuntimeException('No file key given');
249
        }
250
251 6
        $files = $this->psr->getUploadedFiles();
252 6
        $i = 0;
253
254 6
        foreach ($keys as $key) {
255 6
            if (isset($files[$key])) {
256
                /** @var array|PsrUploadedFile */
257 4
                $files = $files[$key];
258 4
                $i++;
259
260 4
                if ($files instanceof PsrUploadedFile) {
261 3
                    if ($i < count($keys)) {
262 1
                        throw new OutOfBoundsException(
263 1
                            'Invalid file key (too deep) ' . $this->formatKeys($keys)
264 1
                        );
265
                    }
266
267 4
                    return $files;
268
                }
269
            } else {
270 2
                throw new OutOfBoundsException('Invalid file key ' . $this->formatKeys($keys));
271
            }
272
        }
273
274 1
        throw new RuntimeException('Multiple files available at key ' . $this->formatKeys($keys));
275
    }
276
277 16
    private function returnOrFail(
278
        array|null $array,
279
        string $key,
280
        mixed $default,
281
        string $error,
282
        int $numArgs
283
    ): mixed {
284
        try {
285 16
            if ((is_null($array) || !isset($array[$key])) && $numArgs > 1) {
286 6
                return $default;
287
            }
288
289 10
            assert(!is_null($array));
290
291 10
            return $array[$key];
292 5
        } catch (Throwable) {
293 5
            throw new OutOfBoundsException("{$error}: '{$key}'");
294
        }
295
    }
296
297
    /** @psalm-param non-empty-list<string> $keys */
298 6
    private function formatKeys(array $keys): string
299
    {
300 6
        return implode('', array_map(
301 6
            fn ($key) => "['" . $key . "']",
302 6
            $keys
303 6
        ));
304
    }
305
306
    /**
307
     * @psalm-param list<list<string>|string> $keys
308
     *
309
     * @psalm-return list<string>
310
     */
311 15
    private function validateKeys(array $keys): array
312
    {
313 15
        if (isset($keys[0]) && is_array($keys[0])) {
314 2
            if (count($keys) > 1) {
315 1
                throw new RuntimeException('Either provide a single array or plain string arguments');
316
            }
317 1
            $keys = $keys[0];
318
        }
319
320
        /** @psalm-var list<string> */
321 14
        return $keys;
322
    }
323
}
324