Chris@0: array of values. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $headers = []; Chris@0: Chris@0: /** Chris@0: * Map of normalized header name to original name used to register header. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $headerNames = []; Chris@0: Chris@0: /** Chris@0: * @var string Chris@0: */ Chris@0: private $protocol = '1.1'; Chris@0: Chris@0: /** Chris@0: * @var StreamInterface Chris@0: */ Chris@0: private $stream; Chris@0: Chris@0: /** Chris@0: * Retrieves the HTTP protocol version as a string. Chris@0: * Chris@0: * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). Chris@0: * Chris@0: * @return string HTTP protocol version. Chris@0: */ Chris@0: public function getProtocolVersion() Chris@0: { Chris@0: return $this->protocol; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Return an instance with the specified HTTP protocol version. Chris@0: * Chris@0: * The version string MUST contain only the HTTP version number (e.g., Chris@0: * "1.1", "1.0"). Chris@0: * Chris@0: * This method MUST be implemented in such a way as to retain the Chris@0: * immutability of the message, and MUST return an instance that has the Chris@0: * new protocol version. Chris@0: * Chris@0: * @param string $version HTTP protocol version Chris@0: * @return static Chris@0: */ Chris@0: public function withProtocolVersion($version) Chris@0: { Chris@0: $this->validateProtocolVersion($version); Chris@0: $new = clone $this; Chris@0: $new->protocol = $version; Chris@0: return $new; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Retrieves all message headers. Chris@0: * Chris@0: * The keys represent the header name as it will be sent over the wire, and Chris@0: * each value is an array of strings associated with the header. Chris@0: * Chris@0: * // Represent the headers as a string Chris@0: * foreach ($message->getHeaders() as $name => $values) { Chris@0: * echo $name . ": " . implode(", ", $values); Chris@0: * } Chris@0: * Chris@0: * // Emit headers iteratively: Chris@0: * foreach ($message->getHeaders() as $name => $values) { Chris@0: * foreach ($values as $value) { Chris@0: * header(sprintf('%s: %s', $name, $value), false); Chris@0: * } Chris@0: * } Chris@0: * Chris@0: * @return array Returns an associative array of the message's headers. Each Chris@0: * key MUST be a header name, and each value MUST be an array of strings. Chris@0: */ Chris@0: public function getHeaders() Chris@0: { Chris@0: return $this->headers; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if a header exists by the given case-insensitive name. Chris@0: * Chris@0: * @param string $header Case-insensitive header name. Chris@0: * @return bool Returns true if any header names match the given header Chris@0: * name using a case-insensitive string comparison. Returns false if Chris@0: * no matching header name is found in the message. Chris@0: */ Chris@0: public function hasHeader($header) Chris@0: { Chris@0: return isset($this->headerNames[strtolower($header)]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Retrieves a message header value by the given case-insensitive name. Chris@0: * Chris@0: * This method returns an array of all the header values of the given Chris@0: * case-insensitive header name. Chris@0: * Chris@0: * If the header does not appear in the message, this method MUST return an Chris@0: * empty array. Chris@0: * Chris@0: * @param string $header Case-insensitive header field name. Chris@0: * @return string[] An array of string values as provided for the given Chris@0: * header. If the header does not appear in the message, this method MUST Chris@0: * return an empty array. Chris@0: */ Chris@0: public function getHeader($header) Chris@0: { Chris@0: if (! $this->hasHeader($header)) { Chris@0: return []; Chris@0: } Chris@0: Chris@0: $header = $this->headerNames[strtolower($header)]; Chris@0: Chris@0: return $this->headers[$header]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Retrieves a comma-separated string of the values for a single header. Chris@0: * Chris@0: * This method returns all of the header values of the given Chris@0: * case-insensitive header name as a string concatenated together using Chris@0: * a comma. Chris@0: * Chris@0: * NOTE: Not all header values may be appropriately represented using Chris@0: * comma concatenation. For such headers, use getHeader() instead Chris@0: * and supply your own delimiter when concatenating. Chris@0: * Chris@0: * If the header does not appear in the message, this method MUST return Chris@0: * an empty string. Chris@0: * Chris@0: * @param string $name Case-insensitive header field name. Chris@0: * @return string A string of values as provided for the given header Chris@0: * concatenated together using a comma. If the header does not appear in Chris@0: * the message, this method MUST return an empty string. Chris@0: */ Chris@0: public function getHeaderLine($name) Chris@0: { Chris@0: $value = $this->getHeader($name); Chris@0: if (empty($value)) { Chris@0: return ''; Chris@0: } Chris@0: Chris@0: return implode(',', $value); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Return an instance with the provided header, replacing any existing Chris@0: * values of any headers with the same case-insensitive name. Chris@0: * Chris@0: * While header names are case-insensitive, the casing of the header will Chris@0: * be preserved by this function, and returned from getHeaders(). Chris@0: * Chris@0: * This method MUST be implemented in such a way as to retain the Chris@0: * immutability of the message, and MUST return an instance that has the Chris@0: * new and/or updated header and value. Chris@0: * Chris@0: * @param string $header Case-insensitive header field name. Chris@0: * @param string|string[] $value Header value(s). Chris@0: * @return static Chris@0: * @throws \InvalidArgumentException for invalid header names or values. Chris@0: */ Chris@0: public function withHeader($header, $value) Chris@0: { Chris@0: $this->assertHeader($header); Chris@0: Chris@0: $normalized = strtolower($header); Chris@0: Chris@0: $new = clone $this; Chris@0: if ($new->hasHeader($header)) { Chris@0: unset($new->headers[$new->headerNames[$normalized]]); Chris@0: } Chris@0: Chris@0: $value = $this->filterHeaderValue($value); Chris@0: Chris@0: $new->headerNames[$normalized] = $header; Chris@0: $new->headers[$header] = $value; Chris@0: Chris@0: return $new; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Return an instance with the specified header appended with the Chris@0: * given value. Chris@0: * Chris@0: * Existing values for the specified header will be maintained. The new Chris@0: * value(s) will be appended to the existing list. If the header did not Chris@0: * exist previously, it will be added. Chris@0: * Chris@0: * This method MUST be implemented in such a way as to retain the Chris@0: * immutability of the message, and MUST return an instance that has the Chris@0: * new header and/or value. Chris@0: * Chris@0: * @param string $header Case-insensitive header field name to add. Chris@0: * @param string|string[] $value Header value(s). Chris@0: * @return static Chris@0: * @throws \InvalidArgumentException for invalid header names or values. Chris@0: */ Chris@0: public function withAddedHeader($header, $value) Chris@0: { Chris@0: $this->assertHeader($header); Chris@0: Chris@0: if (! $this->hasHeader($header)) { Chris@0: return $this->withHeader($header, $value); Chris@0: } Chris@0: Chris@0: $header = $this->headerNames[strtolower($header)]; Chris@0: Chris@0: $new = clone $this; Chris@0: $value = $this->filterHeaderValue($value); Chris@0: $new->headers[$header] = array_merge($this->headers[$header], $value); Chris@0: return $new; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Return an instance without the specified header. Chris@0: * Chris@0: * Header resolution MUST be done without case-sensitivity. Chris@0: * Chris@0: * This method MUST be implemented in such a way as to retain the Chris@0: * immutability of the message, and MUST return an instance that removes Chris@0: * the named header. Chris@0: * Chris@0: * @param string $header Case-insensitive header field name to remove. Chris@0: * @return static Chris@0: */ Chris@0: public function withoutHeader($header) Chris@0: { Chris@0: if (! $this->hasHeader($header)) { Chris@0: return clone $this; Chris@0: } Chris@0: Chris@0: $normalized = strtolower($header); Chris@0: $original = $this->headerNames[$normalized]; Chris@0: Chris@0: $new = clone $this; Chris@0: unset($new->headers[$original], $new->headerNames[$normalized]); Chris@0: return $new; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the body of the message. Chris@0: * Chris@0: * @return StreamInterface Returns the body as a stream. Chris@0: */ Chris@0: public function getBody() Chris@0: { Chris@0: return $this->stream; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Return an instance with the specified message body. Chris@0: * Chris@0: * The body MUST be a StreamInterface object. Chris@0: * Chris@0: * This method MUST be implemented in such a way as to retain the Chris@0: * immutability of the message, and MUST return a new instance that has the Chris@0: * new body stream. Chris@0: * Chris@0: * @param StreamInterface $body Body. Chris@0: * @return static Chris@0: * @throws \InvalidArgumentException When the body is not valid. Chris@0: */ Chris@0: public function withBody(StreamInterface $body) Chris@0: { Chris@0: $new = clone $this; Chris@0: $new->stream = $body; Chris@0: return $new; Chris@0: } Chris@0: Chris@0: private function getStream($stream, $modeIfNotInstance) Chris@0: { Chris@0: if ($stream instanceof StreamInterface) { Chris@0: return $stream; Chris@0: } Chris@0: Chris@0: if (! is_string($stream) && ! is_resource($stream)) { Chris@0: throw new InvalidArgumentException( Chris@0: 'Stream must be a string stream resource identifier, ' Chris@0: . 'an actual stream resource, ' Chris@0: . 'or a Psr\Http\Message\StreamInterface implementation' Chris@0: ); Chris@0: } Chris@0: Chris@0: return new Stream($stream, $modeIfNotInstance); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Filter a set of headers to ensure they are in the correct internal format. Chris@0: * Chris@0: * Used by message constructors to allow setting all initial headers at once. Chris@0: * Chris@0: * @param array $originalHeaders Headers to filter. Chris@0: */ Chris@0: private function setHeaders(array $originalHeaders) Chris@0: { Chris@0: $headerNames = $headers = []; Chris@0: Chris@0: foreach ($originalHeaders as $header => $value) { Chris@0: $value = $this->filterHeaderValue($value); Chris@0: Chris@0: $this->assertHeader($header); Chris@0: Chris@0: $headerNames[strtolower($header)] = $header; Chris@0: $headers[$header] = $value; Chris@0: } Chris@0: Chris@0: $this->headerNames = $headerNames; Chris@0: $this->headers = $headers; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validate the HTTP protocol version Chris@0: * Chris@0: * @param string $version Chris@0: * @throws InvalidArgumentException on invalid HTTP protocol version Chris@0: */ Chris@0: private function validateProtocolVersion($version) Chris@0: { Chris@0: if (empty($version)) { Chris@12: throw new InvalidArgumentException( Chris@0: 'HTTP protocol version can not be empty' Chris@12: ); Chris@0: } Chris@0: if (! is_string($version)) { Chris@0: throw new InvalidArgumentException(sprintf( Chris@0: 'Unsupported HTTP protocol version; must be a string, received %s', Chris@0: (is_object($version) ? get_class($version) : gettype($version)) Chris@0: )); Chris@0: } Chris@0: Chris@0: // HTTP/1 uses a "." numbering scheme to indicate Chris@0: // versions of the protocol, while HTTP/2 does not. Chris@0: if (! preg_match('#^(1\.[01]|2)$#', $version)) { Chris@0: throw new InvalidArgumentException(sprintf( Chris@0: 'Unsupported HTTP protocol version "%s" provided', Chris@0: $version Chris@0: )); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param mixed $values Chris@0: * @return string[] Chris@0: */ Chris@0: private function filterHeaderValue($values) Chris@0: { Chris@0: if (! is_array($values)) { Chris@0: $values = [$values]; Chris@0: } Chris@0: Chris@17: if ([] === $values) { Chris@17: throw new InvalidArgumentException( Chris@17: 'Invalid header value: must be a string or array of strings; ' Chris@17: . 'cannot be an empty array' Chris@17: ); Chris@17: } Chris@17: Chris@0: return array_map(function ($value) { Chris@0: HeaderSecurity::assertValid($value); Chris@0: Chris@0: return (string) $value; Chris@17: }, array_values($values)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Ensure header name and values are valid. Chris@0: * Chris@0: * @param string $name Chris@0: * Chris@0: * @throws InvalidArgumentException Chris@0: */ Chris@0: private function assertHeader($name) Chris@0: { Chris@0: HeaderSecurity::assertValidName($name); Chris@0: } Chris@0: }