Request::checkUploadedFiles()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 17
rs 9.9666
cc 4
nc 4
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of slick/http
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Slick\Http\Message\Server;
13
14
use Psr\Http\Message\ServerRequestInterface;
15
use Psr\Http\Message\StreamInterface;
16
use Psr\Http\Message\UriInterface;
17
use Slick\Http\Message\Exception\InvalidArgumentException;
18
use Slick\Http\Message\Request as HttpRequest;
19
use Slick\Http\Message\Stream\TextStream;
20
use Slick\Http\Message\Uri;
21
22
/**
23
 * Request
24
 *
25
 * @package Slick\Http\Message\Server
26
*/
27
class Request extends HttpRequest implements ServerRequestInterface
28
{
29
    /**
30
     * @var array<string, mixed>
31
     */
32
    private array $server = [];
33
34
    /**
35
     * @var array<string, mixed>
36
     */
37
    private array $cookies = [];
38
39
    /**
40
     * @var array<string, mixed>
41
     */
42
    private array $queryParams = [];
43
44
    /**
45
     * @var null|UploadedFile[]
46
     */
47
    private ?array $uploadedFiles = null;
48
49
    /**
50
     * @var mixed
51
     */
52
    private mixed $parsedBody = '';
53
54
    /**
55
     * @var array<string, mixed>
56
     */
57
    private array $attributes = [];
58
59
    /**
60
     * Creates an HTTP Server Request Message
61
     *
62
     * @param string|null              $method
63
     * @param string|UriInterface|null $target
64
     * @param string|StreamInterface   $body
65
     * @param array<string, string>    $headers
66
     */
67
    public function __construct(
68
        ?string $method = null,
69
        UriInterface|string|null $target = null,
70
        string|StreamInterface|null $body = null,
71
        array $headers = []
72
    ) {
73
        $method = null === $method
74
            ? $this->getServerParams()['REQUEST_METHOD']
75
            : $method;
76
77
        $body = null === $body
78
            ? $this->getPhpInputStream()
79
            : $body;
80
81
        parent::__construct($method, $target, $body, $headers);
82
        $this->loadHeaders();
83
84
        $this->setUri(RequestUriFactory::create($this));
85
    }
86
87
    /**
88
     * Retrieve server parameters.
89
     *
90
     * Retrieves data related to the incoming request environment,
91
     * typically derived from PHP's $_SERVER super-global.
92
     *
93
     * @return array<string, mixed>
94
     * @SuppressWarnings(PHPMD)
95
     */
96
    public function getServerParams(): array
97
    {
98
        if (! $this->server) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->server of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
99
            $this->server = $_SERVER;
100
        }
101
        return $this->server;
102
    }
103
104
    /**
105
     * Retrieve cookies.
106
     *
107
     * Retrieves cookies sent by the client to the server.
108
     *
109
     * @return array<string, mixed>
110
     * @SuppressWarnings(PHPMD)
111
     */
112
    public function getCookieParams(): array
113
    {
114
        if (! $this->cookies) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->cookies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
115
            $this->cookies = $_COOKIE;
116
        }
117
        return $this->cookies;
118
    }
119
120
    /**
121
     * Return an instance with the specified cookies.
122
     *
123
     * @param array<string, mixed> $cookies Array of key/value pairs representing cookies.
124
     * @return Request
125
     */
126
    public function withCookieParams(array $cookies): ServerRequestInterface
127
    {
128
        $request = clone $this;
129
        $request->cookies = $cookies;
130
        return $request;
131
    }
132
133
    /**
134
     * Retrieve query string arguments.
135
     *
136
     * Retrieves the deserialized query string arguments, if any.
137
     *
138
     * @return array<string, mixed>
139
     */
140
    public function getQueryParams(): array
141
    {
142
        if (! $this->queryParams) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queryParams of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
143
            $this->queryParams = $this->detectQueryParams();
144
        }
145
        return $this->queryParams;
146
    }
147
148
    /**
149
     * Return an instance with the specified query string arguments.
150
     *
151
     * @param array<string, mixed> $query Array of query string arguments, typically from
152
     *     $_GET.
153
     *
154
     * @return Request
155
     */
156
    public function withQueryParams(array $query): ServerRequestInterface
157
    {
158
        $request = clone $this;
159
        $request->queryParams = $query;
160
        return $request;
161
    }
162
163
    /**
164
     * Retrieve normalized file upload data.
165
     *
166
     * @return UploadedFile[]
167
     */
168
    public function getUploadedFiles(): array
169
    {
170
        if (null === $this->uploadedFiles) {
171
            $this->uploadedFiles = UploadedFilesFactory::createFiles();
172
        }
173
        return $this->uploadedFiles;
174
    }
175
176
    /**
177
     * Create a new instance with the specified uploaded files.
178
     *
179
     * @param array<array<UploadedFile>>|array<UploadedFile> $uploadedFiles An array tree of
180
     * UploadedFileInterface instances.
181
     * @return Request
182
     *
183
     * @throws InvalidArgumentException if an invalid structure is provided.
184
     */
185
    public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
186
    {
187
        if (! $this->checkUploadedFiles($uploadedFiles)) {
188
            throw new InvalidArgumentException(
189
                "The uploaded files array given has at least one leaf that is not ".
190
                "an UploadedFile object."
191
            );
192
        }
193
194
        $request = clone $this;
195
        $request->uploadedFiles = $uploadedFiles;
0 ignored issues
show
Documentation Bug introduced by
$uploadedFiles is of type array<mixed,Slick\Http\M...\Server\UploadedFile[]>, but the property $uploadedFiles was declared to be of type Slick\Http\Message\Server\UploadedFile[]|null. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
196
        return $request;
197
    }
198
199
200
    /**
201
     * Detects the query params from server and/or request URI
202
     *
203
     * @return array<string, mixed>
204
     * @SuppressWarnings(PHPMD)
205
     */
206
    private function detectQueryParams(): array
207
    {
208
        $uri = new Uri('https://example.org' .$this->getRequestTarget());
209
        parse_str($uri->getQuery(), $params);
210
        return array_merge($_GET, $params);
211
    }
212
213
    /**
214
     * Creates a stream from php input stream
215
     *
216
     * @return TextStream|StreamInterface
217
     */
218
    private function getPhpInputStream(): TextStream|StreamInterface
219
    {
220
        return new TextStream(file_get_contents('php://input'));
221
    }
222
223
    /**
224
     * Check if a provided files array is valid
225
     *
226
     * @param array<UploadedFile|mixed>|array<array<UploadedFile|mixed>> $files
227
     *
228
     * @return bool
229
     */
230
    private function checkUploadedFiles(array $files): bool
231
    {
232
        $valid = true;
233
234
        foreach ($files as $file) {
235
            if (\is_array($file)) {
236
                $valid = $this->checkUploadedFiles($files);
237
                break;
238
            }
239
240
            if (! $file instanceof UploadedFile) {
241
                $valid = false;
242
                break;
243
            }
244
        }
245
246
        return $valid;
247
    }
248
249
    /**
250
     * Loads the headers form request
251
     * @SuppressWarnings(PHPMD)
252
     */
253
    private function loadHeaders(): void
254
    {
255
        foreach ($_SERVER as $key => $value) {
256
            $subset = substr($key, 0, 5);
257
            if ($subset <> 'HTTP_' && $subset <> 'CONTE') {
258
                continue;
259
            }
260
            $header = str_replace(
261
                ' ',
262
                '-',
263
                ucwords(
264
                    str_replace(['http_', '_'], ['', ' '], strtolower($key))
265
                )
266
            );
267
            $this->headers[$this->headerKey($header)] = [$value];
268
        }
269
    }
270
271
    /**
272
     * Retrieve any parameters provided in the request body.
273
     *
274
     * @return null|array<string, mixed>|object The deserialized body parameters, if any.
275
     *     These will typically be an array or object.
276
     */
277
    public function getParsedBody(): object|array|null
278
    {
279
        if (! $this->parsedBody) {
280
            $parser = new BodyParser($this->getHeaderLine('Content-Type'));
281
            $this->parsedBody = $parser->parse($this->getBody());
282
        }
283
        return $this->parsedBody;
284
    }
285
286
    /**
287
     * Return an instance with the specified body parameters.
288
     *
289
     * @param mixed $data The deserialized body data. This will
290
     *     typically be in an array or object.
291
     *
292
     * @return Request
293
     * @throws InvalidArgumentException if an unsupported argument type is
294
     *     provided.
295
     */
296
    public function withParsedBody(mixed $data): ServerRequestInterface
297
    {
298
        if (! \is_null($data) &&
299
            ! \is_array($data) &&
300
            ! \is_object($data)
301
        ) {
302
            throw new InvalidArgumentException(
303
                "Only NULL, array or Object types could be used to ".
304
                "create a new server request message with parsed body."
305
            );
306
        }
307
308
        $request = clone $this;
309
        $request->parsedBody = $data;
310
        return $request;
311
    }
312
313
    /**
314
     * Retrieve attributes derived from the request.
315
     *
316
     * The request "attributes" may be used to allow injection of any
317
     * parameters derived from the request: e.g., the results of path
318
     * match operations; the results of decrypting cookies; the results of
319
     * deserializing non-form-encoded message bodies; etc. Attributes
320
     * will be application- and request-specific, and CAN be mutable.
321
     *
322
     * @return array<string, mixed> Attributes derived from the request.
323
     */
324
    public function getAttributes(): array
325
    {
326
        return $this->attributes;
327
    }
328
329
    /**
330
     * Return an instance with the specified derived request attribute.
331
     *
332
     * This method allows setting a single derived request attribute as
333
     * described in getAttributes().
334
     *
335
     * @param string $name The attribute name.
336
          * @param mixed $value The value of the attribute.
337
     *
338
     * @return Request
339
     *@see getAttributes()
340
     */
341
    public function withAttribute(string $name, $value): ServerRequestInterface
342
    {
343
        $request = clone $this;
344
        $request->attributes[$name] = $value;
345
        return $request;
346
    }
347
348
    /**
349
     * Retrieve a single derived request attribute.
350
     *
351
     * Retrieves a single derived request attribute as described in
352
     * getAttributes(). If the attribute has not been previously set, returns
353
     * the default value as provided.
354
     *
355
     * @see getAttributes()
356
     * @param string $name The attribute name.
357
     * @param mixed $default Default value to return if the attribute does not exist.
358
     * @return mixed
359
     */
360
    public function getAttribute($name, $default = null): mixed
361
    {
362
        if (\array_key_exists($name, $this->attributes)) {
363
            $default = $this->attributes[$name];
364
        }
365
        return $default;
366
    }
367
368
    /**
369
     * Return an instance that removes the specified derived request attribute.
370
     *
371
     * This method allows removing a single derived request attribute as
372
     * described in getAttributes().
373
     *
374
     * @param string $name The attribute name.
375
          *
376
     * @return Request
377
     *@see getAttributes()
378
     */
379
    public function withoutAttribute(string $name): ServerRequestInterface
380
    {
381
        $request = clone $this;
382
        if (\array_key_exists($name, $request->attributes)) {
383
            unset($request->attributes[$name]);
384
        }
385
        return $request;
386
    }
387
}
388