Passed
Push — main ( 8c5b75...682960 )
by Thomas
02:35
created

Request::accept()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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