1<?php 2 3/** 4 * @see https://github.com/laminas/laminas-diactoros for the canonical source repository 5 * @copyright https://github.com/laminas/laminas-diactoros/blob/master/COPYRIGHT.md 6 * @license https://github.com/laminas/laminas-diactoros/blob/master/LICENSE.md New BSD License 7 */ 8 9namespace Laminas\Diactoros; 10 11use InvalidArgumentException; 12use Psr\Http\Message\StreamInterface; 13 14use function array_map; 15use function array_merge; 16use function get_class; 17use function gettype; 18use function implode; 19use function is_array; 20use function is_object; 21use function is_resource; 22use function is_string; 23use function preg_match; 24use function sprintf; 25use function strtolower; 26 27/** 28 * Trait implementing the various methods defined in MessageInterface. 29 * 30 * @see https://github.com/php-fig/http-message/tree/master/src/MessageInterface.php 31 */ 32trait MessageTrait 33{ 34 /** 35 * List of all registered headers, as key => array of values. 36 * 37 * @var array 38 */ 39 protected $headers = []; 40 41 /** 42 * Map of normalized header name to original name used to register header. 43 * 44 * @var array 45 */ 46 protected $headerNames = []; 47 48 /** 49 * @var string 50 */ 51 private $protocol = '1.1'; 52 53 /** 54 * @var StreamInterface 55 */ 56 private $stream; 57 58 /** 59 * Retrieves the HTTP protocol version as a string. 60 * 61 * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). 62 * 63 * @return string HTTP protocol version. 64 */ 65 public function getProtocolVersion() 66 { 67 return $this->protocol; 68 } 69 70 /** 71 * Return an instance with the specified HTTP protocol version. 72 * 73 * The version string MUST contain only the HTTP version number (e.g., 74 * "1.1", "1.0"). 75 * 76 * This method MUST be implemented in such a way as to retain the 77 * immutability of the message, and MUST return an instance that has the 78 * new protocol version. 79 * 80 * @param string $version HTTP protocol version 81 * @return static 82 */ 83 public function withProtocolVersion($version) 84 { 85 $this->validateProtocolVersion($version); 86 $new = clone $this; 87 $new->protocol = $version; 88 return $new; 89 } 90 91 /** 92 * Retrieves all message headers. 93 * 94 * The keys represent the header name as it will be sent over the wire, and 95 * each value is an array of strings associated with the header. 96 * 97 * // Represent the headers as a string 98 * foreach ($message->getHeaders() as $name => $values) { 99 * echo $name . ": " . implode(", ", $values); 100 * } 101 * 102 * // Emit headers iteratively: 103 * foreach ($message->getHeaders() as $name => $values) { 104 * foreach ($values as $value) { 105 * header(sprintf('%s: %s', $name, $value), false); 106 * } 107 * } 108 * 109 * @return array Returns an associative array of the message's headers. Each 110 * key MUST be a header name, and each value MUST be an array of strings. 111 */ 112 public function getHeaders() 113 { 114 return $this->headers; 115 } 116 117 /** 118 * Checks if a header exists by the given case-insensitive name. 119 * 120 * @param string $header Case-insensitive header name. 121 * @return bool Returns true if any header names match the given header 122 * name using a case-insensitive string comparison. Returns false if 123 * no matching header name is found in the message. 124 */ 125 public function hasHeader($header) 126 { 127 return isset($this->headerNames[strtolower($header)]); 128 } 129 130 /** 131 * Retrieves a message header value by the given case-insensitive name. 132 * 133 * This method returns an array of all the header values of the given 134 * case-insensitive header name. 135 * 136 * If the header does not appear in the message, this method MUST return an 137 * empty array. 138 * 139 * @param string $header Case-insensitive header field name. 140 * @return string[] An array of string values as provided for the given 141 * header. If the header does not appear in the message, this method MUST 142 * return an empty array. 143 */ 144 public function getHeader($header) 145 { 146 if (! $this->hasHeader($header)) { 147 return []; 148 } 149 150 $header = $this->headerNames[strtolower($header)]; 151 152 return $this->headers[$header]; 153 } 154 155 /** 156 * Retrieves a comma-separated string of the values for a single header. 157 * 158 * This method returns all of the header values of the given 159 * case-insensitive header name as a string concatenated together using 160 * a comma. 161 * 162 * NOTE: Not all header values may be appropriately represented using 163 * comma concatenation. For such headers, use getHeader() instead 164 * and supply your own delimiter when concatenating. 165 * 166 * If the header does not appear in the message, this method MUST return 167 * an empty string. 168 * 169 * @param string $name Case-insensitive header field name. 170 * @return string A string of values as provided for the given header 171 * concatenated together using a comma. If the header does not appear in 172 * the message, this method MUST return an empty string. 173 */ 174 public function getHeaderLine($name) 175 { 176 $value = $this->getHeader($name); 177 if (empty($value)) { 178 return ''; 179 } 180 181 return implode(',', $value); 182 } 183 184 /** 185 * Return an instance with the provided header, replacing any existing 186 * values of any headers with the same case-insensitive name. 187 * 188 * While header names are case-insensitive, the casing of the header will 189 * be preserved by this function, and returned from getHeaders(). 190 * 191 * This method MUST be implemented in such a way as to retain the 192 * immutability of the message, and MUST return an instance that has the 193 * new and/or updated header and value. 194 * 195 * @param string $header Case-insensitive header field name. 196 * @param string|string[] $value Header value(s). 197 * @return static 198 * @throws \InvalidArgumentException for invalid header names or values. 199 */ 200 public function withHeader($header, $value) 201 { 202 $this->assertHeader($header); 203 204 $normalized = strtolower($header); 205 206 $new = clone $this; 207 if ($new->hasHeader($header)) { 208 unset($new->headers[$new->headerNames[$normalized]]); 209 } 210 211 $value = $this->filterHeaderValue($value); 212 213 $new->headerNames[$normalized] = $header; 214 $new->headers[$header] = $value; 215 216 return $new; 217 } 218 219 /** 220 * Return an instance with the specified header appended with the 221 * given value. 222 * 223 * Existing values for the specified header will be maintained. The new 224 * value(s) will be appended to the existing list. If the header did not 225 * exist previously, it will be added. 226 * 227 * This method MUST be implemented in such a way as to retain the 228 * immutability of the message, and MUST return an instance that has the 229 * new header and/or value. 230 * 231 * @param string $header Case-insensitive header field name to add. 232 * @param string|string[] $value Header value(s). 233 * @return static 234 * @throws \InvalidArgumentException for invalid header names or values. 235 */ 236 public function withAddedHeader($header, $value) 237 { 238 $this->assertHeader($header); 239 240 if (! $this->hasHeader($header)) { 241 return $this->withHeader($header, $value); 242 } 243 244 $header = $this->headerNames[strtolower($header)]; 245 246 $new = clone $this; 247 $value = $this->filterHeaderValue($value); 248 $new->headers[$header] = array_merge($this->headers[$header], $value); 249 return $new; 250 } 251 252 /** 253 * Return an instance without the specified header. 254 * 255 * Header resolution MUST be done without case-sensitivity. 256 * 257 * This method MUST be implemented in such a way as to retain the 258 * immutability of the message, and MUST return an instance that removes 259 * the named header. 260 * 261 * @param string $header Case-insensitive header field name to remove. 262 * @return static 263 */ 264 public function withoutHeader($header) 265 { 266 if (! $this->hasHeader($header)) { 267 return clone $this; 268 } 269 270 $normalized = strtolower($header); 271 $original = $this->headerNames[$normalized]; 272 273 $new = clone $this; 274 unset($new->headers[$original], $new->headerNames[$normalized]); 275 return $new; 276 } 277 278 /** 279 * Gets the body of the message. 280 * 281 * @return StreamInterface Returns the body as a stream. 282 */ 283 public function getBody() 284 { 285 return $this->stream; 286 } 287 288 /** 289 * Return an instance with the specified message body. 290 * 291 * The body MUST be a StreamInterface object. 292 * 293 * This method MUST be implemented in such a way as to retain the 294 * immutability of the message, and MUST return a new instance that has the 295 * new body stream. 296 * 297 * @param StreamInterface $body Body. 298 * @return static 299 * @throws \InvalidArgumentException When the body is not valid. 300 */ 301 public function withBody(StreamInterface $body) 302 { 303 $new = clone $this; 304 $new->stream = $body; 305 return $new; 306 } 307 308 private function getStream($stream, $modeIfNotInstance) 309 { 310 if ($stream instanceof StreamInterface) { 311 return $stream; 312 } 313 314 if (! is_string($stream) && ! is_resource($stream)) { 315 throw new InvalidArgumentException( 316 'Stream must be a string stream resource identifier, ' 317 . 'an actual stream resource, ' 318 . 'or a Psr\Http\Message\StreamInterface implementation' 319 ); 320 } 321 322 return new Stream($stream, $modeIfNotInstance); 323 } 324 325 /** 326 * Filter a set of headers to ensure they are in the correct internal format. 327 * 328 * Used by message constructors to allow setting all initial headers at once. 329 * 330 * @param array $originalHeaders Headers to filter. 331 */ 332 private function setHeaders(array $originalHeaders) 333 { 334 $headerNames = $headers = []; 335 336 foreach ($originalHeaders as $header => $value) { 337 $value = $this->filterHeaderValue($value); 338 339 $this->assertHeader($header); 340 341 $headerNames[strtolower($header)] = $header; 342 $headers[$header] = $value; 343 } 344 345 $this->headerNames = $headerNames; 346 $this->headers = $headers; 347 } 348 349 /** 350 * Validate the HTTP protocol version 351 * 352 * @param string $version 353 * @throws InvalidArgumentException on invalid HTTP protocol version 354 */ 355 private function validateProtocolVersion($version) 356 { 357 if (empty($version)) { 358 throw new InvalidArgumentException( 359 'HTTP protocol version can not be empty' 360 ); 361 } 362 if (! is_string($version)) { 363 throw new InvalidArgumentException(sprintf( 364 'Unsupported HTTP protocol version; must be a string, received %s', 365 (is_object($version) ? get_class($version) : gettype($version)) 366 )); 367 } 368 369 // HTTP/1 uses a "<major>.<minor>" numbering scheme to indicate 370 // versions of the protocol, while HTTP/2 does not. 371 if (! preg_match('#^(1\.[01]|2)$#', $version)) { 372 throw new InvalidArgumentException(sprintf( 373 'Unsupported HTTP protocol version "%s" provided', 374 $version 375 )); 376 } 377 } 378 379 /** 380 * @param mixed $values 381 * @return string[] 382 */ 383 private function filterHeaderValue($values) 384 { 385 if (! is_array($values)) { 386 $values = [$values]; 387 } 388 389 if ([] === $values) { 390 throw new InvalidArgumentException( 391 'Invalid header value: must be a string or array of strings; ' 392 . 'cannot be an empty array' 393 ); 394 } 395 396 return array_map(function ($value) { 397 HeaderSecurity::assertValid($value); 398 399 return (string) $value; 400 }, array_values($values)); 401 } 402 403 /** 404 * Ensure header name and values are valid. 405 * 406 * @param string $name 407 * 408 * @throws InvalidArgumentException 409 */ 410 private function assertHeader($name) 411 { 412 HeaderSecurity::assertValidName($name); 413 } 414} 415