ServerRequest::getContentType()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
crap 2
1
<?php
2
namespace Kambo\Http\Message;
3
4
// \Spl
5
use InvalidArgumentException;
6
7
// \Psr
8
use Psr\Http\Message\ServerRequestInterface;
9
use Psr\Http\Message\UriInterface;
10
use Psr\Http\Message\StreamInterface;
11
use Psr\Http\Message\UploadedFileInterface;
12
13
// \Http\Message
14
use Kambo\Http\Message\Uri;
15
use Kambo\Http\Message\Message;
16
use Kambo\Http\Message\UploadedFile;
17
use Kambo\Http\Message\Headers;
18
use Kambo\Http\Message\Parser\Parser;
19
use Kambo\Http\Message\RequestTrait;
20
21
/**
22
 * Representation of an incoming, server-side HTTP request.
23
 *
24
 * Per the HTTP specification, this class includes properties for
25
 * each of the following:
26
 *
27
 * - Protocol version
28
 * - HTTP method
29
 * - URI
30
 * - Headers
31
 * - Message body
32
 *
33
 * Additionally, it encapsulates all data as it has arrived to the
34
 * application from the CGI and/or PHP environment, including:
35
 *
36
 * - The values represented in $_SERVER.
37
 * - Any cookies provided (generally via $_COOKIE)
38
 * - Query string arguments (generally via $_GET, or as parsed via parse_str())
39
 * - Upload files, if any (as represented by $_FILES)
40
 * - Deserialized body parameters (generally from $_POST)
41
 *
42
 * $_SERVER values are treated as immutable, as they represent application
43
 * state at the time of request; as such, no methods are provided to allow
44
 * modification of those values. The other values provide such methods, as they
45
 * can be restored from $_SERVER or the request body, and may need treatment
46
 * during the application (e.g., body parameters may be deserialized based on
47
 * content type).
48
 *
49
 * Additionally, this class recognizes the utility of introspecting a
50
 * request to derive and match additional parameters (e.g., via URI path
51
 * matching, decrypting cookie values, deserializing non-form-encoded body
52
 * content, matching authorization headers to users, etc). These parameters
53
 * are stored in an "attributes" property.
54
 *
55
 * Requests are considered immutable; all methods that change state retain
56
 * the internal state of the current message and return an instance that
57
 * contains the changed state.
58
 *
59
 * @package Kambo\Http\Message
60
 * @author  Bohuslav Simek <[email protected]>
61
 * @license MIT
62
 */
63
class ServerRequest extends Message implements ServerRequestInterface
64
{
65
    use RequestTrait;
66
67
    /**
68
     * Server parameters - related to the incoming request environment, 
69
     * they are typically derived from PHP's $_SERVER superglobal.
70
     *
71
     * @var array
72
     */
73
    private $serverVariables;
74
75
    /**
76
     * Deserialized query string arguments, if any.
77
     *
78
     * @var array
79
     */
80
    private $queryParams = null;
81
82
    /**
83
     * Cookies sent by the client to the server.
84
     *
85
     * @var array
86
     */
87
    private $cookies;
88
89
    /**
90
     * Contain attributes derived from the request.
91
     *
92
     * @var array
93
     */
94
    private $attributes = [];
95
96
    /**
97
     * Uploaded files of incoming request, if any.
98
     *
99
     * @var array
100
     */
101
    private $uploadedFiles = null;
102
103
    /**
104
     * Parsed incoming request body - this value is filled 
105
     * when method getParsedBody or withParsedBody is called.
106
     *
107
     * @var array|object|null
108
     */
109
    private $parsedBody = null;
110
111
    /**
112
     * Create new HTTP request.
113
     *
114
     * Adds a host header when none was provided and a host is defined in uri.
115
     * 
116
     * @param string          $requestMethod   The request method
117
     * @param UriInterface    $uri             The request URI object
118
     * @param StreamInterface $body            The request body object
119
     * @param Headers|array   $headers         The request headers collection
120
     * @param array           $serverVariables The server environment variables
121
     * @param array           $cookies         The request cookies collection
122
     * @param array           $uploadFiles     The request uploadedFiles collection
123
     * @param string          $protocol        The request version of the protocol
124
     * @param array           $attributes      The request attributs
125
     *
126
     */
127 28
    public function __construct(
128
        $requestMethod,
129
        Uri $uri,
130
        StreamInterface $body,
131
        $headers,
132
        array $serverVariables,
133
        array $cookies,
134
        array $uploadFiles,
135
        $protocol,
136
        array $attributes = []
137
    ) {
138 28
        parent::__construct($headers, $body, $protocol);
139 28
        $this->validateMethod($requestMethod);
140 28
        $this->uri             = $uri;
141 28
        $this->cookies         = $cookies;
142 28
        $this->requestMethod   = $requestMethod;
143 28
        $this->uploadedFiles   = $uploadFiles;
144 28
        $this->attributes      = $attributes;
145 28
        $this->serverVariables = $serverVariables;
146 28
    }
147
148
    /**
149
     * Retrieve server parameters.
150
     *
151
     * Retrieves data related to the incoming request environment,
152
     * typically derived from PHP's $_SERVER superglobal. The data IS NOT
153
     * REQUIRED to originate from $_SERVER.
154
     *
155
     * @return array
156
     */
157 1
    public function getServerParams()
158
    {
159 1
        return $this->serverVariables;
160
    }
161
162
    /**
163
     * Retrieve cookies.
164
     *
165
     * Retrieves cookies sent by the client to the server.
166
     *
167
     * The data are compatible with the structure of the $_COOKIE
168
     * superglobal.
169
     *
170
     * @return array
171
     */
172 2
    public function getCookieParams()
173
    {
174 2
        return $this->cookies;
175
    }
176
177
    /**
178
     * Return an instance with the specified cookies.
179
     *
180
     * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
181
     * be compatible with the structure of $_COOKIE. Typically, this data will
182
     * be injected at instantiation.
183
     *
184
     * This method not update the related Cookie header of the request
185
     * instance, nor related values in the server params.
186
     *
187
     * This method retain the immutability of the message, and return an instance 
188
     * that has the updated cookie values.
189
     *
190
     * @param array $cookies Array of key/value pairs representing cookies.
191
     *
192
     * @return self for fluent interface
193
     */
194 1
    public function withCookieParams(array $cookies)
195
    {
196 1
        $clone          = clone $this;
197 1
        $clone->cookies = $cookies;
198
199 1
        return $clone;
200
    }
201
202
    /**
203
     * Retrieve query string arguments.
204
     *
205
     * Retrieves the deserialized query string arguments, if any.
206
     *
207
     * Note: the query params might not be in sync with the URI or server
208
     * params. If you need to ensure you are only getting the original
209
     * values, you may need to parse the query string from `getUri()->getQuery()`
210
     * or from the `QUERY_STRING` server param.
211
     *
212
     * @return array
213
     */
214 2
    public function getQueryParams()
215
    {
216 2
        if ($this->queryParams === null) {
217 2
            parse_str($this->uri->getQuery(), $this->queryParams);
218 2
        }
219
220 2
        return $this->queryParams;
221
    }
222
223
    /**
224
     * Return an instance with the specified query string arguments.
225
     *
226
     * These values remain immutable over the course of the incoming
227
     * request.
228
     *
229
     * Setting query string arguments not change the URI stored by the
230
     * request, nor the values in the server params.
231
     *
232
     * This method retain the immutability of the message, and return an instance 
233
     * that has the updated query string arguments.
234
     *
235
     * @param array $query Array of query string arguments, typically from $_GET.
236
     *
237
     * @return self for fluent interface
238
     */
239 1
    public function withQueryParams(array $query)
240
    {
241 1
        $clone              = clone $this;
242 1
        $clone->queryParams = $query;
243
244 1
        return $clone;
245
    }
246
247
    /**
248
     * Retrieve normalized file upload data.
249
     *
250
     * This method returns upload metadata in a normalized tree, with each leaf
251
     * an instance of Psr\Http\Message\UploadedFileInterface.
252
     *
253
     * These values can be prepared from $_FILES or the message body during
254
     * instantiation, or can be injected via withUploadedFiles().
255
     *
256
     * @return array An array tree of UploadedFileInterface instances; an empty
257
     *               array is returned if no data is present.
258
     */
259 2
    public function getUploadedFiles()
260
    {
261 2
        return $this->uploadedFiles;
262
    }
263
264
    /**
265
     * Create a new instance with the specified uploaded files.
266
     *
267
     * This method retain the immutability of the message, and return an instance 
268
     * that has the updated body parameters.
269
     *
270
     * @param array An array tree of UploadedFileInterface instances.
271
     *
272
     * @return self for fluent interface
273
     *
274
     * @throws \InvalidArgumentException if an invalid structure is provided.
275
     */
276 2
    public function withUploadedFiles(array $uploadedFiles)
277
    {
278 2
        $this->validateUploadedFiles($uploadedFiles);
279 1
        $clone                = clone $this;
280 1
        $clone->uploadedFiles = $uploadedFiles;
281
282 1
        return $clone;
283
    }
284
285
    /**
286
     * Retrieve any parameters provided in the request body.
287
     *
288
     * If the request Content-Type is either application/x-www-form-urlencoded
289
     * or multipart/form-data, and the request method is POST, this method
290
     * return the contents of $_POST.
291
     *
292
     * Otherwise, this method may return any results of deserializing
293
     * the request body content; as parsing returns structured content, the
294
     * potential types are arrays or objects. A null value indicates
295
     * the absence of body content.
296
     *
297
     * @return null|array|object The deserialized body parameters, if any.
298
     *                           These will typically be an array or object.
299
     */
300 5
    public function getParsedBody()
301
    {
302 5
        if ($this->body !== null && $this->parsedBody === null) {
303 3
            $parser = new Parser($this->getContentType());
304 3
            $this->parsedBody = $parser->parse($this->body);
305 3
        }
306
307 5
        return $this->parsedBody;
308
    }
309
310
    /**
311
     * Return an instance with the specified body parameters.
312
     *
313
     * If the request Content-Type is either application/x-www-form-urlencoded
314
     * or multipart/form-data, and the request method is POST, use this method
315
     * ONLY to inject the contents of $_POST.
316
     *
317
     * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
318
     * deserializing the request body content. Deserialization/parsing returns
319
     * structured data, and, as such, this method ONLY accepts arrays or objects,
320
     * or a null value if nothing was available to parse.
321
     *
322
     * As an example, if content negotiation determines that the request data
323
     * is a JSON payload, this method could be used to create a request
324
     * instance with the deserialized parameters.
325
     *
326
     * This method retain the immutability of the message, and return an 
327
     * instance that has the updated body parameters.
328
     *     
329
     * @param null|array|object $data The deserialized body data. This will
330
     *                                typically be in an array or object.
331
     *
332
     * @return self for fluent interface
333
     *
334
     * @throws \InvalidArgumentException if an unsupported argument type is provided.    
335
     */
336 4
    public function withParsedBody($data)
337
    {
338 4
        if (!is_null($data) && !is_object($data) && !is_array($data)) {
339 1
            throw new InvalidArgumentException('Value must be an array, an object, or null');
340
        }
341
342 3
        $clone             = clone $this;
343 3
        $clone->parsedBody = $data;
344
345 3
        return $clone;
346
    }
347
348
    /**
349
     * Retrieve attributes derived from the request.
350
     *
351
     * The request "attributes" may be used to allow injection of any
352
     * parameters derived from the request: e.g., the results of path
353
     * match operations; the results of decrypting cookies; the results of
354
     * deserializing non-form-encoded message bodies; etc. Attributes
355
     * will be application and request specific, and is mutable.
356
     *
357
     * @return array Attributes derived from the request.
358
     */
359 1
    public function getAttributes()
360
    {
361 1
        return $this->attributes;
362
    }
363
364
    /**
365
     * Retrieve a single derived request attribute.
366
     *
367
     * Retrieves a single derived request attribute as described in
368
     * getAttributes(). If the attribute has not been previously set, returns
369
     * the default value as provided.
370
     *
371
     * This method obviates the need for a hasAttribute() method, as it allows
372
     * specifying a default value to return if the attribute is not found.
373
     *
374
     * @see getAttributes()
375
     *
376
     * @param string $name    The attribute name.
377
     * @param mixed  $default Default value to return if the attribute does not exist.
378
     *
379
     * @return mixed
380
     */
381 4
    public function getAttribute($name, $default = null)
382
    {
383 4
        $attributeValue = $default;
384 4
        if (isset($this->attributes[$name])) {
385 3
            $attributeValue = $this->attributes[$name];
386 3
        }
387
388 4
        return $attributeValue;
389
    }
390
391
    /**
392
     * Return an instance with the specified derived request attribute.
393
     *
394
     * This method allows setting a single derived request attribute as
395
     * described in getAttributes().
396
     *
397
     * This method retains the state of the current instance, and return
398
     * an instance that has the updated attribute.
399
     *
400
     * @see getAttributes()
401
     *
402
     * @param string $name  The attribute name.
403
     * @param mixed  $value The value of the attribute.
404
     *
405
     * @return self for fluent interface
406
     */
407 4
    public function withAttribute($name, $value)
408
    {
409 4
        $clone                    = clone $this;
410 4
        $clone->attributes[$name] = $value;
411
412 4
        return $clone;
413
    }
414
415
    /**
416
     * Return an instance that removes the specified derived request attribute.
417
     *
418
     * This method allows removing a single derived request attribute as
419
     * described in getAttributes().
420
     *
421
     * This method retains the state of the current instance, and return
422
     * an instance that removes the attribute.
423
     *
424
     * @see getAttributes()
425
     *
426
     * @param string $name The attribute name.
427
     *
428
     * @return self for fluent interface
429
     */
430 1
    public function withoutAttribute($name)
431
    {
432 1
        $clone = clone $this;
433
434 1
        if (isset($clone->attributes[$name])) {
435 1
            unset($clone->attributes[$name]);
436 1
        }
437
438 1
        return $clone;
439
    }
440
441
    // ------------ PRIVATE METHODS
442
443
    /**
444
     * Validate request method
445
     *
446
     * @param string $method request method
447
     *
448
     * @return self for fluent interface
449
     *
450
     * @throws \InvalidArgumentException if an unsupported method is provided.     
451
     */
452 28
    private function validateMethod($method)
453
    {
454
        $valid = [
455 28
            'GET'    => true,
456 28
            'POST'   => true,
457 28
            'DELETE' => true,
458 28
            'PUT'    => true,
459
            'PATCH'  => true
460 28
        ];
461
462 28
        if (!isset($valid[$method])) {
463 1
            throw new InvalidArgumentException(
464
                'Invalid method version. Must be one of: GET, POST, DELTE, PUT or PATCH'
465 1
            );
466
        }
467 28
    }
468
469
    /**
470
     * Get request content type.
471
     *
472
     * @return string|null The request content type, if known
473
     */
474 3
    private function getContentType()
475
    {
476 3
        $result = null;
477 3
        if ($this->hasHeader('Content-Type')) {
478 2
            $result = $this->getHeader('Content-Type')[0];
479 2
        }
480
481 3
        return $result;
482
    }
483
484
    /**
485
     * Validate the structure in an uploaded files array.
486
     *
487
     * @param array $uploadedFiles
488
     *
489
     * @throws InvalidArgumentException if any entry is not an UploadedFileInterface instance.
490
     */
491 2
    private function validateUploadedFiles(array $uploadedFiles)
492
    {
493 2
        foreach ($uploadedFiles as $file) {
494 2
            if (is_array($file)) {
495 1
                $this->validateUploadedFiles($file);
496 2
            } elseif (!$file instanceof UploadedFileInterface) {
497 1
                throw new InvalidArgumentException('Invalid entry in uploaded files structure');
498
            }
499 1
        }
500 1
    }
501
}
502