Passed
Push — main ( 83e8ed...8c5b75 )
by Thomas
13:12
created

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