Passed
Push — main ( ed64d2...c44892 )
by Thomas
02:43
created

Request::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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