Request::set()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
ccs 3
cts 3
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
/** @psalm-api */
18
class Request
19
{
20
    use WrapsMessage;
21
    use WrapsRequest;
22
23 181
    public function __construct(protected PsrServerRequest $psr)
24
    {
25 181
    }
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 6
    public function accept(): array
115
    {
116 6
        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
     * Psalm does not support multi file uploads yet and complains
230
     * about type issues. We need to suppres some of the errors.
231
     *
232
     * @no-named-arguments
233
     *
234
     * @psalm-param list<non-empty-string>|string ...$keys
235
     *
236
     * @throws OutOfBoundsException RuntimeException
237
     */
238 7
    public function file(array|string ...$keys): PsrUploadedFile
239
    {
240 7
        $keys = $this->validateKeys($keys);
241
242 7
        if (count($keys) === 0) {
243 1
            throw new RuntimeException('No file key given');
244
        }
245
246 6
        $files = $this->psr->getUploadedFiles();
247 6
        $i = 0;
248
249 6
        foreach ($keys as $key) {
250 6
            if (isset($files[$key])) {
251
                /** @var array|PsrUploadedFile */
252 4
                $files = $files[$key];
253 4
                $i++;
254
255 4
                if ($files instanceof PsrUploadedFile) {
256 3
                    if ($i < count($keys)) {
257 1
                        throw new OutOfBoundsException(
258 1
                            'Invalid file key (too deep) ' . $this->formatKeys($keys)
259 1
                        );
260
                    }
261
262 4
                    return $files;
263
                }
264
            } else {
265 2
                throw new OutOfBoundsException('Invalid file key ' . $this->formatKeys($keys));
266
            }
267
        }
268
269 1
        throw new RuntimeException('Multiple files available at key ' . $this->formatKeys($keys));
270
    }
271
272 16
    private function returnOrFail(
273
        array|null $array,
274
        string $key,
275
        mixed $default,
276
        string $error,
277
        int $numArgs
278
    ): mixed {
279
        try {
280 16
            if ((is_null($array) || !isset($array[$key])) && $numArgs > 1) {
281 6
                return $default;
282
            }
283
284 10
            assert(!is_null($array));
285
286 10
            return $array[$key];
287 5
        } catch (Throwable) {
288 5
            throw new OutOfBoundsException("{$error}: '{$key}'");
289
        }
290
    }
291
292
    /** @psalm-param non-empty-list<string> $keys */
293 6
    private function formatKeys(array $keys): string
294
    {
295 6
        return implode('', array_map(
296 6
            fn ($key) => "['" . $key . "']",
297 6
            $keys
298 6
        ));
299
    }
300
301
    /**
302
     * @psalm-param list<list<string>|string> $keys
303
     *
304
     * @psalm-return list<string>
305
     */
306 15
    private function validateKeys(array $keys): array
307
    {
308 15
        if (isset($keys[0]) && is_array($keys[0])) {
309 2
            if (count($keys) > 1) {
310 1
                throw new RuntimeException('Either provide a single array or plain string arguments');
311
            }
312 1
            $keys = $keys[0];
313
        }
314
315
        /** @psalm-var list<string> */
316 14
        return $keys;
317
    }
318
}
319