Skip to content

Commit

Permalink
Merge pull request #518 from clue-labs/psr7-response
Browse files Browse the repository at this point in the history
Refactor `Response` class to build on top of new PSR-7 implementation
  • Loading branch information
WyriHaximus committed Mar 14, 2024
2 parents 33a0cf3 + 518ca68 commit 4c23ea4
Show file tree
Hide file tree
Showing 8 changed files with 516 additions and 17 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2448,8 +2448,7 @@ constants with the `STATUS_*` prefix. For instance, the `200 OK` and
`404 Not Found` status codes can used as `Response::STATUS_OK` and
`Response::STATUS_NOT_FOUND` respectively.

> Internally, this implementation builds on top of an existing incoming
response message and only adds required streaming support. This base class is
> Internally, this implementation builds on top of a base class which is
considered an implementation detail that may change in the future.

##### html()
Expand Down
164 changes: 164 additions & 0 deletions src/Io/AbstractMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

namespace React\Http\Io;

use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;

/**
* [Internal] Abstract HTTP message base class (PSR-7)
*
* @internal
* @see MessageInterface
*/
abstract class AbstractMessage implements MessageInterface
{
/** @var array<string,string[]> */
private $headers = array();

/** @var array<string,string> */
private $headerNamesLowerCase = array();

/** @var string */
private $protocolVersion;

/** @var StreamInterface */
private $body;

/**
* @param string $protocolVersion
* @param array<string,string|string[]> $headers
* @param StreamInterface $body
*/
protected function __construct($protocolVersion, array $headers, StreamInterface $body)
{
foreach ($headers as $name => $value) {
if ($value !== array()) {
if (\is_array($value)) {
foreach ($value as &$one) {
$one = (string) $one;
}
} else {
$value = array((string) $value);
}

$lower = \strtolower($name);
if (isset($this->headerNamesLowerCase[$lower])) {
$value = \array_merge($this->headers[$this->headerNamesLowerCase[$lower]], $value);
unset($this->headers[$this->headerNamesLowerCase[$lower]]);
}

$this->headers[$name] = $value;
$this->headerNamesLowerCase[$lower] = $name;
}
}

$this->protocolVersion = (string) $protocolVersion;
$this->body = $body;
}

public function getProtocolVersion()
{
return $this->protocolVersion;
}

public function withProtocolVersion($version)
{
if ((string) $version === $this->protocolVersion) {
return $this;
}

$message = clone $this;
$message->protocolVersion = (string) $version;

return $message;
}

public function getHeaders()
{
return $this->headers;
}

public function hasHeader($name)
{
return isset($this->headerNamesLowerCase[\strtolower($name)]);
}

public function getHeader($name)
{
$lower = \strtolower($name);
return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array();
}

public function getHeaderLine($name)
{
return \implode(', ', $this->getHeader($name));
}

public function withHeader($name, $value)
{
if ($value === array()) {
return $this->withoutHeader($name);
} elseif (\is_array($value)) {
foreach ($value as &$one) {
$one = (string) $one;
}
} else {
$value = array((string) $value);
}

$lower = \strtolower($name);
if (isset($this->headerNamesLowerCase[$lower]) && $this->headerNamesLowerCase[$lower] === (string) $name && $this->headers[$this->headerNamesLowerCase[$lower]] === $value) {
return $this;
}

$message = clone $this;
if (isset($message->headerNamesLowerCase[$lower])) {
unset($message->headers[$message->headerNamesLowerCase[$lower]]);
}

$message->headers[$name] = $value;
$message->headerNamesLowerCase[$lower] = $name;

return $message;
}

public function withAddedHeader($name, $value)
{
if ($value === array()) {
return $this;
}

return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value)));
}

public function withoutHeader($name)
{
$lower = \strtolower($name);
if (!isset($this->headerNamesLowerCase[$lower])) {
return $this;
}

$message = clone $this;
unset($message->headers[$message->headerNamesLowerCase[$lower]], $message->headerNamesLowerCase[$lower]);

return $message;
}

public function getBody()
{
return $this->body;
}

public function withBody(StreamInterface $body)
{
if ($body === $this->body) {
return $this;
}

$message = clone $this;
$message->body = $body;

return $message;
}
}
103 changes: 92 additions & 11 deletions src/Message/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
namespace React\Http\Message;

use Fig\Http\Message\StatusCodeInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use React\Http\Io\AbstractMessage;
use React\Http\Io\BufferedBody;
use React\Http\Io\HttpBodyStream;
use React\Stream\ReadableStreamInterface;
use RingCentral\Psr7\Response as Psr7Response;

/**
* Represents an outgoing server response message.
Expand All @@ -34,13 +35,12 @@
* `404 Not Found` status codes can used as `Response::STATUS_OK` and
* `Response::STATUS_NOT_FOUND` respectively.
*
* > Internally, this implementation builds on top of an existing incoming
* response message and only adds required streaming support. This base class is
* > Internally, this implementation builds on top a base class which is
* considered an implementation detail that may change in the future.
*
* @see \Psr\Http\Message\ResponseInterface
*/
final class Response extends Psr7Response implements StatusCodeInterface
final class Response extends AbstractMessage implements ResponseInterface, StatusCodeInterface
{
/**
* Create an HTML response
Expand Down Expand Up @@ -257,6 +257,41 @@ public static function xml($xml)
return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml);
}

/**
* @var bool
* @see self::$phrasesMap
*/
private static $phrasesInitialized = false;

/**
* Map of standard HTTP status codes to standard reason phrases.
*
* This map will be fully populated with all standard reason phrases on
* first access. By default, it only contains a subset of HTTP status codes
* that have a custom mapping to reason phrases (such as those with dashes
* and all caps words). See `self::STATUS_*` for all possible status code
* constants.
*
* @var array<int,string>
* @see self::STATUS_*
* @see self::getReasonPhraseForStatusCode()
*/
private static $phrasesMap = array(
200 => 'OK',
203 => 'Non-Authoritative Information',
207 => 'Multi-Status',
226 => 'IM Used',
414 => 'URI Too Large',
418 => 'I\'m a teapot',
505 => 'HTTP Version Not Supported'
);

/** @var int */
private $statusCode;

/** @var string */
private $reasonPhrase;

/**
* @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants
* @param array<string,string|string[]> $headers additional response headers
Expand All @@ -280,12 +315,58 @@ public function __construct(
throw new \InvalidArgumentException('Invalid response body given');
}

parent::__construct(
$status,
$headers,
$body,
$version,
$reason
);
parent::__construct($version, $headers, $body);

$this->statusCode = (int) $status;
$this->reasonPhrase = ($reason !== '' && $reason !== null) ? (string) $reason : self::getReasonPhraseForStatusCode($status);
}

public function getStatusCode()
{
return $this->statusCode;
}

public function withStatus($code, $reasonPhrase = '')
{
if ((string) $reasonPhrase === '') {
$reasonPhrase = self::getReasonPhraseForStatusCode($code);
}

if ($this->statusCode === (int) $code && $this->reasonPhrase === (string) $reasonPhrase) {
return $this;
}

$response = clone $this;
$response->statusCode = (int) $code;
$response->reasonPhrase = (string) $reasonPhrase;

return $response;
}

public function getReasonPhrase()
{
return $this->reasonPhrase;
}

/**
* @param int $code
* @return string default reason phrase for given status code or empty string if unknown
*/
private static function getReasonPhraseForStatusCode($code)
{
if (!self::$phrasesInitialized) {
self::$phrasesInitialized = true;

// map all `self::STATUS_` constants from status code to reason phrase
// e.g. `self::STATUS_NOT_FOUND = 404` will be mapped to `404 Not Found`
$ref = new \ReflectionClass(__CLASS__);
foreach ($ref->getConstants() as $name => $value) {
if (!isset(self::$phrasesMap[$value]) && \strpos($name, 'STATUS_') === 0) {
self::$phrasesMap[$value] = \ucwords(\strtolower(\str_replace('_', ' ', \substr($name, 7))));
}
}
}

return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : '';
}
}

0 comments on commit 4c23ea4

Please sign in to comment.