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