PageRenderTime 104ms CodeModel.GetById 60ms app.highlight 35ms RepoModel.GetById 1ms app.codeStats 0ms

/Slim/Http/Request.php

http://github.com/codeguy/Slim
PHP | 1217 lines | 471 code | 123 blank | 623 comment | 52 complexity | 5201b403c8b396606a41d871d66a479b MD5 | raw file
   1<?php
   2/**
   3 * Slim Framework (https://slimframework.com)
   4 *
   5 * @link      https://github.com/slimphp/Slim
   6 * @copyright Copyright (c) 2011-2017 Josh Lockhart
   7 * @license   https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License)
   8 */
   9namespace Slim\Http;
  10
  11use Closure;
  12use InvalidArgumentException;
  13use Psr\Http\Message\UploadedFileInterface;
  14use RuntimeException;
  15use Psr\Http\Message\ServerRequestInterface;
  16use Psr\Http\Message\UriInterface;
  17use Psr\Http\Message\StreamInterface;
  18use Slim\Collection;
  19use Slim\Exception\InvalidMethodException;
  20use Slim\Exception\MethodNotAllowedException;
  21use Slim\Interfaces\Http\HeadersInterface;
  22
  23/**
  24 * Request
  25 *
  26 * This class represents an HTTP request. It manages
  27 * the request method, URI, headers, cookies, and body
  28 * according to the PSR-7 standard.
  29 *
  30 * @link https://github.com/php-fig/http-message/blob/master/src/MessageInterface.php
  31 * @link https://github.com/php-fig/http-message/blob/master/src/RequestInterface.php
  32 * @link https://github.com/php-fig/http-message/blob/master/src/ServerRequestInterface.php
  33 */
  34class Request extends Message implements ServerRequestInterface
  35{
  36    /**
  37     * The request method
  38     *
  39     * @var string
  40     */
  41    protected $method;
  42
  43    /**
  44     * The original request method (ignoring override)
  45     *
  46     * @var string
  47     */
  48    protected $originalMethod;
  49
  50    /**
  51     * The request URI object
  52     *
  53     * @var \Psr\Http\Message\UriInterface
  54     */
  55    protected $uri;
  56
  57    /**
  58     * The request URI target (path + query string)
  59     *
  60     * @var string
  61     */
  62    protected $requestTarget;
  63
  64    /**
  65     * The request query string params
  66     *
  67     * @var array
  68     */
  69    protected $queryParams;
  70
  71    /**
  72     * The request cookies
  73     *
  74     * @var array
  75     */
  76    protected $cookies;
  77
  78    /**
  79     * The server environment variables at the time the request was created.
  80     *
  81     * @var array
  82     */
  83    protected $serverParams;
  84
  85    /**
  86     * The request attributes (route segment names and values)
  87     *
  88     * @var \Slim\Collection
  89     */
  90    protected $attributes;
  91
  92    /**
  93     * The request body parsed (if possible) into a PHP array or object
  94     *
  95     * @var null|array|object
  96     */
  97    protected $bodyParsed = false;
  98
  99    /**
 100     * List of request body parsers (e.g., url-encoded, JSON, XML, multipart)
 101     *
 102     * @var callable[]
 103     */
 104    protected $bodyParsers = [];
 105
 106    /**
 107     * List of uploaded files
 108     *
 109     * @var UploadedFileInterface[]
 110     */
 111    protected $uploadedFiles;
 112
 113    /**
 114     * Valid request methods
 115     *
 116     * @var string[]
 117     * @deprecated
 118     */
 119    protected $validMethods = [
 120        'CONNECT' => 1,
 121        'DELETE' => 1,
 122        'GET' => 1,
 123        'HEAD' => 1,
 124        'OPTIONS' => 1,
 125        'PATCH' => 1,
 126        'POST' => 1,
 127        'PUT' => 1,
 128        'TRACE' => 1,
 129    ];
 130
 131    /**
 132     * Create new HTTP request with data extracted from the application
 133     * Environment object
 134     *
 135     * @param  Environment $environment The Slim application Environment
 136     *
 137     * @return static
 138     */
 139    public static function createFromEnvironment(Environment $environment)
 140    {
 141        $method = $environment['REQUEST_METHOD'];
 142        $uri = Uri::createFromEnvironment($environment);
 143        $headers = Headers::createFromEnvironment($environment);
 144        $cookies = Cookies::parseHeader($headers->get('Cookie', []));
 145        $serverParams = $environment->all();
 146        $body = new RequestBody();
 147        $uploadedFiles = UploadedFile::createFromEnvironment($environment);
 148
 149        $request = new static($method, $uri, $headers, $cookies, $serverParams, $body, $uploadedFiles);
 150
 151        if ($method === 'POST' &&
 152            in_array($request->getMediaType(), ['application/x-www-form-urlencoded', 'multipart/form-data'])
 153        ) {
 154            // parsed body must be $_POST
 155            $request = $request->withParsedBody($_POST);
 156        }
 157        return $request;
 158    }
 159
 160    /**
 161     * Create new HTTP request.
 162     *
 163     * Adds a host header when none was provided and a host is defined in uri.
 164     *
 165     * @param string           $method        The request method
 166     * @param UriInterface     $uri           The request URI object
 167     * @param HeadersInterface $headers       The request headers collection
 168     * @param array            $cookies       The request cookies collection
 169     * @param array            $serverParams  The server environment variables
 170     * @param StreamInterface  $body          The request body object
 171     * @param array            $uploadedFiles The request uploadedFiles collection
 172     * @throws InvalidMethodException on invalid HTTP method
 173     */
 174    public function __construct(
 175        $method,
 176        UriInterface $uri,
 177        HeadersInterface $headers,
 178        array $cookies,
 179        array $serverParams,
 180        StreamInterface $body,
 181        array $uploadedFiles = []
 182    ) {
 183        try {
 184            $this->originalMethod = $this->filterMethod($method);
 185        } catch (InvalidMethodException $e) {
 186            $this->originalMethod = $method;
 187        }
 188
 189        $this->uri = $uri;
 190        $this->headers = $headers;
 191        $this->cookies = $cookies;
 192        $this->serverParams = $serverParams;
 193        $this->attributes = new Collection();
 194        $this->body = $body;
 195        $this->uploadedFiles = $uploadedFiles;
 196
 197        if (isset($serverParams['SERVER_PROTOCOL'])) {
 198            $this->protocolVersion = str_replace('HTTP/', '', $serverParams['SERVER_PROTOCOL']);
 199        }
 200
 201        if (!$this->headers->has('Host') || $this->uri->getHost() !== '') {
 202            $this->headers->set('Host', $this->uri->getHost());
 203        }
 204
 205        $this->registerMediaTypeParser('application/json', function ($input) {
 206            $result = json_decode($input, true);
 207            if (!is_array($result)) {
 208                return null;
 209            }
 210            return $result;
 211        });
 212
 213        $this->registerMediaTypeParser('application/xml', function ($input) {
 214            $backup = libxml_disable_entity_loader(true);
 215            $backup_errors = libxml_use_internal_errors(true);
 216            $result = simplexml_load_string($input);
 217            libxml_disable_entity_loader($backup);
 218            libxml_clear_errors();
 219            libxml_use_internal_errors($backup_errors);
 220            if ($result === false) {
 221                return null;
 222            }
 223            return $result;
 224        });
 225
 226        $this->registerMediaTypeParser('text/xml', function ($input) {
 227            $backup = libxml_disable_entity_loader(true);
 228            $backup_errors = libxml_use_internal_errors(true);
 229            $result = simplexml_load_string($input);
 230            libxml_disable_entity_loader($backup);
 231            libxml_clear_errors();
 232            libxml_use_internal_errors($backup_errors);
 233            if ($result === false) {
 234                return null;
 235            }
 236            return $result;
 237        });
 238
 239        $this->registerMediaTypeParser('application/x-www-form-urlencoded', function ($input) {
 240            parse_str($input, $data);
 241            return $data;
 242        });
 243
 244        // if the request had an invalid method, we can throw it now
 245        if (isset($e) && $e instanceof InvalidMethodException) {
 246            throw $e;
 247        }
 248    }
 249
 250    /**
 251     * This method is applied to the cloned object
 252     * after PHP performs an initial shallow-copy. This
 253     * method completes a deep-copy by creating new objects
 254     * for the cloned object's internal reference pointers.
 255     */
 256    public function __clone()
 257    {
 258        $this->headers = clone $this->headers;
 259        $this->attributes = clone $this->attributes;
 260        $this->body = clone $this->body;
 261    }
 262
 263    /*******************************************************************************
 264     * Method
 265     ******************************************************************************/
 266
 267    /**
 268     * Retrieves the HTTP method of the request.
 269     *
 270     * @return string Returns the request method.
 271     */
 272    public function getMethod()
 273    {
 274        if ($this->method === null) {
 275            $this->method = $this->originalMethod;
 276            $customMethod = $this->getHeaderLine('X-Http-Method-Override');
 277
 278            if ($customMethod) {
 279                $this->method = $this->filterMethod($customMethod);
 280            } elseif ($this->originalMethod === 'POST') {
 281                $overrideMethod = $this->filterMethod($this->getParsedBodyParam('_METHOD'));
 282                if ($overrideMethod !== null) {
 283                    $this->method = $overrideMethod;
 284                }
 285
 286                if ($this->getBody()->eof()) {
 287                    $this->getBody()->rewind();
 288                }
 289            }
 290        }
 291
 292        return $this->method;
 293    }
 294
 295    /**
 296     * Get the original HTTP method (ignore override).
 297     *
 298     * Note: This method is not part of the PSR-7 standard.
 299     *
 300     * @return string
 301     */
 302    public function getOriginalMethod()
 303    {
 304        return $this->originalMethod;
 305    }
 306
 307    /**
 308     * Return an instance with the provided HTTP method.
 309     *
 310     * While HTTP method names are typically all uppercase characters, HTTP
 311     * method names are case-sensitive and thus implementations SHOULD NOT
 312     * modify the given string.
 313     *
 314     * This method MUST be implemented in such a way as to retain the
 315     * immutability of the message, and MUST return an instance that has the
 316     * changed request method.
 317     *
 318     * @param string $method Case-sensitive method.
 319     * @return static
 320     * @throws \InvalidArgumentException for invalid HTTP methods.
 321     */
 322    public function withMethod($method)
 323    {
 324        $method = $this->filterMethod($method);
 325        $clone = clone $this;
 326        $clone->originalMethod = $method;
 327        $clone->method = $method;
 328
 329        return $clone;
 330    }
 331
 332    /**
 333     * Validate the HTTP method
 334     *
 335     * @param  null|string $method
 336     * @return null|string
 337     * @throws \InvalidArgumentException on invalid HTTP method.
 338     */
 339    protected function filterMethod($method)
 340    {
 341        if ($method === null) {
 342            return $method;
 343        }
 344
 345        if (!is_string($method)) {
 346            throw new InvalidArgumentException(sprintf(
 347                'Unsupported HTTP method; must be a string, received %s',
 348                (is_object($method) ? get_class($method) : gettype($method))
 349            ));
 350        }
 351
 352        $method = strtoupper($method);
 353        if (preg_match("/^[!#$%&'*+.^_`|~0-9a-z-]+$/i", $method) !== 1) {
 354            throw new InvalidMethodException($this, $method);
 355        }
 356
 357        return $method;
 358    }
 359
 360    /**
 361     * Does this request use a given method?
 362     *
 363     * Note: This method is not part of the PSR-7 standard.
 364     *
 365     * @param  string $method HTTP method
 366     * @return bool
 367     */
 368    public function isMethod($method)
 369    {
 370        return $this->getMethod() === $method;
 371    }
 372
 373    /**
 374     * Is this a GET request?
 375     *
 376     * Note: This method is not part of the PSR-7 standard.
 377     *
 378     * @return bool
 379     */
 380    public function isGet()
 381    {
 382        return $this->isMethod('GET');
 383    }
 384
 385    /**
 386     * Is this a POST request?
 387     *
 388     * Note: This method is not part of the PSR-7 standard.
 389     *
 390     * @return bool
 391     */
 392    public function isPost()
 393    {
 394        return $this->isMethod('POST');
 395    }
 396
 397    /**
 398     * Is this a PUT request?
 399     *
 400     * Note: This method is not part of the PSR-7 standard.
 401     *
 402     * @return bool
 403     */
 404    public function isPut()
 405    {
 406        return $this->isMethod('PUT');
 407    }
 408
 409    /**
 410     * Is this a PATCH request?
 411     *
 412     * Note: This method is not part of the PSR-7 standard.
 413     *
 414     * @return bool
 415     */
 416    public function isPatch()
 417    {
 418        return $this->isMethod('PATCH');
 419    }
 420
 421    /**
 422     * Is this a DELETE request?
 423     *
 424     * Note: This method is not part of the PSR-7 standard.
 425     *
 426     * @return bool
 427     */
 428    public function isDelete()
 429    {
 430        return $this->isMethod('DELETE');
 431    }
 432
 433    /**
 434     * Is this a HEAD request?
 435     *
 436     * Note: This method is not part of the PSR-7 standard.
 437     *
 438     * @return bool
 439     */
 440    public function isHead()
 441    {
 442        return $this->isMethod('HEAD');
 443    }
 444
 445    /**
 446     * Is this a OPTIONS request?
 447     *
 448     * Note: This method is not part of the PSR-7 standard.
 449     *
 450     * @return bool
 451     */
 452    public function isOptions()
 453    {
 454        return $this->isMethod('OPTIONS');
 455    }
 456
 457    /**
 458     * Is this an XHR request?
 459     *
 460     * Note: This method is not part of the PSR-7 standard.
 461     *
 462     * @return bool
 463     */
 464    public function isXhr()
 465    {
 466        return $this->getHeaderLine('X-Requested-With') === 'XMLHttpRequest';
 467    }
 468
 469    /*******************************************************************************
 470     * URI
 471     ******************************************************************************/
 472
 473    /**
 474     * Retrieves the message's request target.
 475     *
 476     * Retrieves the message's request-target either as it will appear (for
 477     * clients), as it appeared at request (for servers), or as it was
 478     * specified for the instance (see withRequestTarget()).
 479     *
 480     * In most cases, this will be the origin-form of the composed URI,
 481     * unless a value was provided to the concrete implementation (see
 482     * withRequestTarget() below).
 483     *
 484     * If no URI is available, and no request-target has been specifically
 485     * provided, this method MUST return the string "/".
 486     *
 487     * @return string
 488     */
 489    public function getRequestTarget()
 490    {
 491        if ($this->requestTarget) {
 492            return $this->requestTarget;
 493        }
 494
 495        if ($this->uri === null) {
 496            return '/';
 497        }
 498
 499        $basePath = $this->uri->getBasePath();
 500        $path = $this->uri->getPath();
 501        $path = $basePath . '/' . ltrim($path, '/');
 502
 503        $query = $this->uri->getQuery();
 504        if ($query) {
 505            $path .= '?' . $query;
 506        }
 507        $this->requestTarget = $path;
 508
 509        return $this->requestTarget;
 510    }
 511
 512    /**
 513     * Return an instance with the specific request-target.
 514     *
 515     * If the request needs a non-origin-form request-target — e.g., for
 516     * specifying an absolute-form, authority-form, or asterisk-form —
 517     * this method may be used to create an instance with the specified
 518     * request-target, verbatim.
 519     *
 520     * This method MUST be implemented in such a way as to retain the
 521     * immutability of the message, and MUST return an instance that has the
 522     * changed request target.
 523     *
 524     * @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
 525     *     request-target forms allowed in request messages)
 526     * @param mixed $requestTarget
 527     * @return static
 528     * @throws InvalidArgumentException if the request target is invalid
 529     */
 530    public function withRequestTarget($requestTarget)
 531    {
 532        if (preg_match('#\s#', $requestTarget)) {
 533            throw new InvalidArgumentException(
 534                'Invalid request target provided; must be a string and cannot contain whitespace'
 535            );
 536        }
 537        $clone = clone $this;
 538        $clone->requestTarget = $requestTarget;
 539
 540        return $clone;
 541    }
 542
 543    /**
 544     * Retrieves the URI instance.
 545     *
 546     * This method MUST return a UriInterface instance.
 547     *
 548     * @link http://tools.ietf.org/html/rfc3986#section-4.3
 549     * @return UriInterface Returns a UriInterface instance
 550     *     representing the URI of the request.
 551     */
 552    public function getUri()
 553    {
 554        return $this->uri;
 555    }
 556
 557    /**
 558     * Returns an instance with the provided URI.
 559     *
 560     * This method MUST update the Host header of the returned request by
 561     * default if the URI contains a host component. If the URI does not
 562     * contain a host component, any pre-existing Host header MUST be carried
 563     * over to the returned request.
 564     *
 565     * You can opt-in to preserving the original state of the Host header by
 566     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
 567     * `true`, this method interacts with the Host header in the following ways:
 568     *
 569     * - If the the Host header is missing or empty, and the new URI contains
 570     *   a host component, this method MUST update the Host header in the returned
 571     *   request.
 572     * - If the Host header is missing or empty, and the new URI does not contain a
 573     *   host component, this method MUST NOT update the Host header in the returned
 574     *   request.
 575     * - If a Host header is present and non-empty, this method MUST NOT update
 576     *   the Host header in the returned request.
 577     *
 578     * This method MUST be implemented in such a way as to retain the
 579     * immutability of the message, and MUST return an instance that has the
 580     * new UriInterface instance.
 581     *
 582     * @link http://tools.ietf.org/html/rfc3986#section-4.3
 583     * @param UriInterface $uri New request URI to use.
 584     * @param bool $preserveHost Preserve the original state of the Host header.
 585     * @return static
 586     */
 587    public function withUri(UriInterface $uri, $preserveHost = false)
 588    {
 589        $clone = clone $this;
 590        $clone->uri = $uri;
 591
 592        if (!$preserveHost) {
 593            if ($uri->getHost() !== '') {
 594                $clone->headers->set('Host', $uri->getHost());
 595            }
 596        } else {
 597            if ($uri->getHost() !== '' && (!$this->hasHeader('Host') || $this->getHeaderLine('Host') === '')) {
 598                $clone->headers->set('Host', $uri->getHost());
 599            }
 600        }
 601
 602        return $clone;
 603    }
 604
 605    /**
 606     * Get request content type.
 607     *
 608     * Note: This method is not part of the PSR-7 standard.
 609     *
 610     * @return string|null The request content type, if known
 611     */
 612    public function getContentType()
 613    {
 614        $result = $this->getHeader('Content-Type');
 615
 616        return $result ? $result[0] : null;
 617    }
 618
 619    /**
 620     * Get request media type, if known.
 621     *
 622     * Note: This method is not part of the PSR-7 standard.
 623     *
 624     * @return string|null The request media type, minus content-type params
 625     */
 626    public function getMediaType()
 627    {
 628        $contentType = $this->getContentType();
 629        if ($contentType) {
 630            $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType);
 631
 632            return strtolower($contentTypeParts[0]);
 633        }
 634
 635        return null;
 636    }
 637
 638    /**
 639     * Get request media type params, if known.
 640     *
 641     * Note: This method is not part of the PSR-7 standard.
 642     *
 643     * @return array
 644     */
 645    public function getMediaTypeParams()
 646    {
 647        $contentType = $this->getContentType();
 648        $contentTypeParams = [];
 649        if ($contentType) {
 650            $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType);
 651            $contentTypePartsLength = count($contentTypeParts);
 652            for ($i = 1; $i < $contentTypePartsLength; $i++) {
 653                $paramParts = explode('=', $contentTypeParts[$i]);
 654                $contentTypeParams[strtolower($paramParts[0])] = $paramParts[1];
 655            }
 656        }
 657
 658        return $contentTypeParams;
 659    }
 660
 661    /**
 662     * Get request content character set, if known.
 663     *
 664     * Note: This method is not part of the PSR-7 standard.
 665     *
 666     * @return string|null
 667     */
 668    public function getContentCharset()
 669    {
 670        $mediaTypeParams = $this->getMediaTypeParams();
 671        if (isset($mediaTypeParams['charset'])) {
 672            return $mediaTypeParams['charset'];
 673        }
 674
 675        return null;
 676    }
 677
 678    /**
 679     * Get request content length, if known.
 680     *
 681     * Note: This method is not part of the PSR-7 standard.
 682     *
 683     * @return int|null
 684     */
 685    public function getContentLength()
 686    {
 687        $result = $this->headers->get('Content-Length');
 688
 689        return $result ? (int)$result[0] : null;
 690    }
 691
 692    /*******************************************************************************
 693     * Cookies
 694     ******************************************************************************/
 695
 696    /**
 697     * Retrieve cookies.
 698     *
 699     * Retrieves cookies sent by the client to the server.
 700     *
 701     * The data MUST be compatible with the structure of the $_COOKIE
 702     * superglobal.
 703     *
 704     * @return array
 705     */
 706    public function getCookieParams()
 707    {
 708        return $this->cookies;
 709    }
 710
 711    /**
 712     * Fetch cookie value from cookies sent by the client to the server.
 713     *
 714     * Note: This method is not part of the PSR-7 standard.
 715     *
 716     * @param string $key     The attribute name.
 717     * @param mixed  $default Default value to return if the attribute does not exist.
 718     *
 719     * @return mixed
 720     */
 721    public function getCookieParam($key, $default = null)
 722    {
 723        $cookies = $this->getCookieParams();
 724        $result = $default;
 725        if (isset($cookies[$key])) {
 726            $result = $cookies[$key];
 727        }
 728
 729        return $result;
 730    }
 731
 732    /**
 733     * Return an instance with the specified cookies.
 734     *
 735     * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
 736     * be compatible with the structure of $_COOKIE. Typically, this data will
 737     * be injected at instantiation.
 738     *
 739     * This method MUST NOT update the related Cookie header of the request
 740     * instance, nor related values in the server params.
 741     *
 742     * This method MUST be implemented in such a way as to retain the
 743     * immutability of the message, and MUST return an instance that has the
 744     * updated cookie values.
 745     *
 746     * @param array $cookies Array of key/value pairs representing cookies.
 747     * @return static
 748     */
 749    public function withCookieParams(array $cookies)
 750    {
 751        $clone = clone $this;
 752        $clone->cookies = $cookies;
 753
 754        return $clone;
 755    }
 756
 757    /*******************************************************************************
 758     * Query Params
 759     ******************************************************************************/
 760
 761    /**
 762     * Retrieve query string arguments.
 763     *
 764     * Retrieves the deserialized query string arguments, if any.
 765     *
 766     * Note: the query params might not be in sync with the URI or server
 767     * params. If you need to ensure you are only getting the original
 768     * values, you may need to parse the query string from `getUri()->getQuery()`
 769     * or from the `QUERY_STRING` server param.
 770     *
 771     * @return array
 772     */
 773    public function getQueryParams()
 774    {
 775        if (is_array($this->queryParams)) {
 776            return $this->queryParams;
 777        }
 778
 779        if ($this->uri === null) {
 780            return [];
 781        }
 782
 783        parse_str($this->uri->getQuery(), $this->queryParams); // <-- URL decodes data
 784
 785        return $this->queryParams;
 786    }
 787
 788    /**
 789     * Return an instance with the specified query string arguments.
 790     *
 791     * These values SHOULD remain immutable over the course of the incoming
 792     * request. They MAY be injected during instantiation, such as from PHP's
 793     * $_GET superglobal, or MAY be derived from some other value such as the
 794     * URI. In cases where the arguments are parsed from the URI, the data
 795     * MUST be compatible with what PHP's parse_str() would return for
 796     * purposes of how duplicate query parameters are handled, and how nested
 797     * sets are handled.
 798     *
 799     * Setting query string arguments MUST NOT change the URI stored by the
 800     * request, nor the values in the server params.
 801     *
 802     * This method MUST be implemented in such a way as to retain the
 803     * immutability of the message, and MUST return an instance that has the
 804     * updated query string arguments.
 805     *
 806     * @param array $query Array of query string arguments, typically from
 807     *     $_GET.
 808     * @return static
 809     */
 810    public function withQueryParams(array $query)
 811    {
 812        $clone = clone $this;
 813        $clone->queryParams = $query;
 814
 815        return $clone;
 816    }
 817
 818    /*******************************************************************************
 819     * File Params
 820     ******************************************************************************/
 821
 822    /**
 823     * Retrieve normalized file upload data.
 824     *
 825     * This method returns upload metadata in a normalized tree, with each leaf
 826     * an instance of Psr\Http\Message\UploadedFileInterface.
 827     *
 828     * These values MAY be prepared from $_FILES or the message body during
 829     * instantiation, or MAY be injected via withUploadedFiles().
 830     *
 831     * @return array An array tree of UploadedFileInterface instances; an empty
 832     *     array MUST be returned if no data is present.
 833     */
 834    public function getUploadedFiles()
 835    {
 836        return $this->uploadedFiles;
 837    }
 838
 839    /**
 840     * Create a new instance with the specified uploaded files.
 841     *
 842     * This method MUST be implemented in such a way as to retain the
 843     * immutability of the message, and MUST return an instance that has the
 844     * updated body parameters.
 845     *
 846     * @param array $uploadedFiles An array tree of UploadedFileInterface instances.
 847     * @return static
 848     * @throws \InvalidArgumentException if an invalid structure is provided.
 849     */
 850    public function withUploadedFiles(array $uploadedFiles)
 851    {
 852        $clone = clone $this;
 853        $clone->uploadedFiles = $uploadedFiles;
 854
 855        return $clone;
 856    }
 857
 858    /*******************************************************************************
 859     * Server Params
 860     ******************************************************************************/
 861
 862    /**
 863     * Retrieve server parameters.
 864     *
 865     * Retrieves data related to the incoming request environment,
 866     * typically derived from PHP's $_SERVER superglobal. The data IS NOT
 867     * REQUIRED to originate from $_SERVER.
 868     *
 869     * @return array
 870     */
 871    public function getServerParams()
 872    {
 873        return $this->serverParams;
 874    }
 875
 876    /**
 877     * Retrieve a server parameter.
 878     *
 879     * Note: This method is not part of the PSR-7 standard.
 880     *
 881     * @param  string $key
 882     * @param  mixed  $default
 883     * @return mixed
 884     */
 885    public function getServerParam($key, $default = null)
 886    {
 887        $serverParams = $this->getServerParams();
 888
 889        return isset($serverParams[$key]) ? $serverParams[$key] : $default;
 890    }
 891
 892    /*******************************************************************************
 893     * Attributes
 894     ******************************************************************************/
 895
 896    /**
 897     * Retrieve attributes derived from the request.
 898     *
 899     * The request "attributes" may be used to allow injection of any
 900     * parameters derived from the request: e.g., the results of path
 901     * match operations; the results of decrypting cookies; the results of
 902     * deserializing non-form-encoded message bodies; etc. Attributes
 903     * will be application and request specific, and CAN be mutable.
 904     *
 905     * @return array Attributes derived from the request.
 906     */
 907    public function getAttributes()
 908    {
 909        return $this->attributes->all();
 910    }
 911
 912    /**
 913     * Retrieve a single derived request attribute.
 914     *
 915     * Retrieves a single derived request attribute as described in
 916     * getAttributes(). If the attribute has not been previously set, returns
 917     * the default value as provided.
 918     *
 919     * This method obviates the need for a hasAttribute() method, as it allows
 920     * specifying a default value to return if the attribute is not found.
 921     *
 922     * @see getAttributes()
 923     * @param string $name The attribute name.
 924     * @param mixed $default Default value to return if the attribute does not exist.
 925     * @return mixed
 926     */
 927    public function getAttribute($name, $default = null)
 928    {
 929        return $this->attributes->get($name, $default);
 930    }
 931
 932    /**
 933     * Return an instance with the specified derived request attribute.
 934     *
 935     * This method allows setting a single derived request attribute as
 936     * described in getAttributes().
 937     *
 938     * This method MUST be implemented in such a way as to retain the
 939     * immutability of the message, and MUST return an instance that has the
 940     * updated attribute.
 941     *
 942     * @see getAttributes()
 943     * @param string $name The attribute name.
 944     * @param mixed $value The value of the attribute.
 945     * @return static
 946     */
 947    public function withAttribute($name, $value)
 948    {
 949        $clone = clone $this;
 950        $clone->attributes->set($name, $value);
 951
 952        return $clone;
 953    }
 954
 955    /**
 956     * Create a new instance with the specified derived request attributes.
 957     *
 958     * Note: This method is not part of the PSR-7 standard.
 959     *
 960     * This method allows setting all new derived request attributes as
 961     * described in getAttributes().
 962     *
 963     * This method MUST be implemented in such a way as to retain the
 964     * immutability of the message, and MUST return a new instance that has the
 965     * updated attributes.
 966     *
 967     * @param  array $attributes New attributes
 968     * @return static
 969     */
 970    public function withAttributes(array $attributes)
 971    {
 972        $clone = clone $this;
 973        $clone->attributes = new Collection($attributes);
 974
 975        return $clone;
 976    }
 977
 978    /**
 979     * Return an instance that removes the specified derived request attribute.
 980     *
 981     * This method allows removing a single derived request attribute as
 982     * described in getAttributes().
 983     *
 984     * This method MUST be implemented in such a way as to retain the
 985     * immutability of the message, and MUST return an instance that removes
 986     * the attribute.
 987     *
 988     * @see getAttributes()
 989     * @param string $name The attribute name.
 990     * @return static
 991     */
 992    public function withoutAttribute($name)
 993    {
 994        $clone = clone $this;
 995        $clone->attributes->remove($name);
 996
 997        return $clone;
 998    }
 999
1000    /*******************************************************************************
1001     * Body
1002     ******************************************************************************/
1003
1004    /**
1005     * Retrieve any parameters provided in the request body.
1006     *
1007     * If the request Content-Type is either application/x-www-form-urlencoded
1008     * or multipart/form-data, and the request method is POST, this method MUST
1009     * return the contents of $_POST.
1010     *
1011     * Otherwise, this method may return any results of deserializing
1012     * the request body content; as parsing returns structured content, the
1013     * potential types MUST be arrays or objects only. A null value indicates
1014     * the absence of body content.
1015     *
1016     * @return null|array|object The deserialized body parameters, if any.
1017     *     These will typically be an array or object.
1018     * @throws RuntimeException if the request body media type parser returns an invalid value
1019     */
1020    public function getParsedBody()
1021    {
1022        if ($this->bodyParsed !== false) {
1023            return $this->bodyParsed;
1024        }
1025
1026        if (!$this->body) {
1027            return null;
1028        }
1029
1030        $mediaType = $this->getMediaType();
1031
1032        // look for a media type with a structured syntax suffix (RFC 6839)
1033        $parts = explode('+', $mediaType);
1034        if (count($parts) >= 2) {
1035            $mediaType = 'application/' . $parts[count($parts)-1];
1036        }
1037
1038        if (isset($this->bodyParsers[$mediaType]) === true) {
1039            $body = (string)$this->getBody();
1040            $parsed = $this->bodyParsers[$mediaType]($body);
1041
1042            if (!is_null($parsed) && !is_object($parsed) && !is_array($parsed)) {
1043                throw new RuntimeException(
1044                    'Request body media type parser return value must be an array, an object, or null'
1045                );
1046            }
1047            $this->bodyParsed = $parsed;
1048            return $this->bodyParsed;
1049        }
1050
1051        return null;
1052    }
1053
1054    /**
1055     * Return an instance with the specified body parameters.
1056     *
1057     * These MAY be injected during instantiation.
1058     *
1059     * If the request Content-Type is either application/x-www-form-urlencoded
1060     * or multipart/form-data, and the request method is POST, use this method
1061     * ONLY to inject the contents of $_POST.
1062     *
1063     * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
1064     * deserializing the request body content. Deserialization/parsing returns
1065     * structured data, and, as such, this method ONLY accepts arrays or objects,
1066     * or a null value if nothing was available to parse.
1067     *
1068     * As an example, if content negotiation determines that the request data
1069     * is a JSON payload, this method could be used to create a request
1070     * instance with the deserialized parameters.
1071     *
1072     * This method MUST be implemented in such a way as to retain the
1073     * immutability of the message, and MUST return an instance that has the
1074     * updated body parameters.
1075     *
1076     * @param null|array|object $data The deserialized body data. This will
1077     *     typically be in an array or object.
1078     * @return static
1079     * @throws \InvalidArgumentException if an unsupported argument type is
1080     *     provided.
1081     */
1082    public function withParsedBody($data)
1083    {
1084        if (!is_null($data) && !is_object($data) && !is_array($data)) {
1085            throw new InvalidArgumentException('Parsed body value must be an array, an object, or null');
1086        }
1087
1088        $clone = clone $this;
1089        $clone->bodyParsed = $data;
1090
1091        return $clone;
1092    }
1093
1094    /**
1095     * Force Body to be parsed again.
1096     *
1097     * Note: This method is not part of the PSR-7 standard.
1098     *
1099     * @return $this
1100     */
1101    public function reparseBody()
1102    {
1103        $this->bodyParsed = false;
1104
1105        return $this;
1106    }
1107
1108    /**
1109     * Register media type parser.
1110     *
1111     * Note: This method is not part of the PSR-7 standard.
1112     *
1113     * @param string   $mediaType A HTTP media type (excluding content-type
1114     *     params).
1115     * @param callable $callable  A callable that returns parsed contents for
1116     *     media type.
1117     */
1118    public function registerMediaTypeParser($mediaType, callable $callable)
1119    {
1120        if ($callable instanceof Closure) {
1121            $callable = $callable->bindTo($this);
1122        }
1123        $this->bodyParsers[(string)$mediaType] = $callable;
1124    }
1125
1126    /*******************************************************************************
1127     * Parameters (e.g., POST and GET data)
1128     ******************************************************************************/
1129
1130    /**
1131     * Fetch request parameter value from body or query string (in that order).
1132     *
1133     * Note: This method is not part of the PSR-7 standard.
1134     *
1135     * @param  string $key The parameter key.
1136     * @param  string $default The default value.
1137     *
1138     * @return mixed The parameter value.
1139     */
1140    public function getParam($key, $default = null)
1141    {
1142        $postParams = $this->getParsedBody();
1143        $getParams = $this->getQueryParams();
1144        $result = $default;
1145        if (is_array($postParams) && isset($postParams[$key])) {
1146            $result = $postParams[$key];
1147        } elseif (is_object($postParams) && property_exists($postParams, $key)) {
1148            $result = $postParams->$key;
1149        } elseif (isset($getParams[$key])) {
1150            $result = $getParams[$key];
1151        }
1152
1153        return $result;
1154    }
1155
1156    /**
1157     * Fetch parameter value from request body.
1158     *
1159     * Note: This method is not part of the PSR-7 standard.
1160     *
1161     * @param string $key
1162     * @param mixed $default
1163     *
1164     * @return mixed
1165     */
1166    public function getParsedBodyParam($key, $default = null)
1167    {
1168        $postParams = $this->getParsedBody();
1169        $result = $default;
1170        if (is_array($postParams) && isset($postParams[$key])) {
1171            $result = $postParams[$key];
1172        } elseif (is_object($postParams) && property_exists($postParams, $key)) {
1173            $result = $postParams->$key;
1174        }
1175
1176        return $result;
1177    }
1178
1179    /**
1180     * Fetch parameter value from query string.
1181     *
1182     * Note: This method is not part of the PSR-7 standard.
1183     *
1184     * @param string $key
1185     * @param mixed $default
1186     *
1187     * @return mixed
1188     */
1189    public function getQueryParam($key, $default = null)
1190    {
1191        $getParams = $this->getQueryParams();
1192        $result = $default;
1193        if (isset($getParams[$key])) {
1194            $result = $getParams[$key];
1195        }
1196
1197        return $result;
1198    }
1199
1200    /**
1201     * Fetch associative array of body and query string parameters.
1202     *
1203     * Note: This method is not part of the PSR-7 standard.
1204     *
1205     * @return array
1206     */
1207    public function getParams()
1208    {
1209        $params = $this->getQueryParams();
1210        $postParams = $this->getParsedBody();
1211        if ($postParams) {
1212            $params = array_merge($params, (array)$postParams);
1213        }
1214
1215        return $params;
1216    }
1217}