Passed
Push — main ( 682960...085d3b )
by Thomas
02:23
created

Request::wrap()   A

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