Skip to content

Commit 4c23ea4

Browse files
authored
Merge pull request #518 from clue-labs/psr7-response
Refactor `Response` class to build on top of new PSR-7 implementation
2 parents 33a0cf3 + 518ca68 commit 4c23ea4

File tree

8 files changed

+516
-17
lines changed

8 files changed

+516
-17
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2448,8 +2448,7 @@ constants with the `STATUS_*` prefix. For instance, the `200 OK` and
24482448
`404 Not Found` status codes can used as `Response::STATUS_OK` and
24492449
`Response::STATUS_NOT_FOUND` respectively.
24502450

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

24552454
##### html()

src/Io/AbstractMessage.php

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
namespace React\Http\Io;
4+
5+
use Psr\Http\Message\MessageInterface;
6+
use Psr\Http\Message\StreamInterface;
7+
8+
/**
9+
* [Internal] Abstract HTTP message base class (PSR-7)
10+
*
11+
* @internal
12+
* @see MessageInterface
13+
*/
14+
abstract class AbstractMessage implements MessageInterface
15+
{
16+
/** @var array<string,string[]> */
17+
private $headers = array();
18+
19+
/** @var array<string,string> */
20+
private $headerNamesLowerCase = array();
21+
22+
/** @var string */
23+
private $protocolVersion;
24+
25+
/** @var StreamInterface */
26+
private $body;
27+
28+
/**
29+
* @param string $protocolVersion
30+
* @param array<string,string|string[]> $headers
31+
* @param StreamInterface $body
32+
*/
33+
protected function __construct($protocolVersion, array $headers, StreamInterface $body)
34+
{
35+
foreach ($headers as $name => $value) {
36+
if ($value !== array()) {
37+
if (\is_array($value)) {
38+
foreach ($value as &$one) {
39+
$one = (string) $one;
40+
}
41+
} else {
42+
$value = array((string) $value);
43+
}
44+
45+
$lower = \strtolower($name);
46+
if (isset($this->headerNamesLowerCase[$lower])) {
47+
$value = \array_merge($this->headers[$this->headerNamesLowerCase[$lower]], $value);
48+
unset($this->headers[$this->headerNamesLowerCase[$lower]]);
49+
}
50+
51+
$this->headers[$name] = $value;
52+
$this->headerNamesLowerCase[$lower] = $name;
53+
}
54+
}
55+
56+
$this->protocolVersion = (string) $protocolVersion;
57+
$this->body = $body;
58+
}
59+
60+
public function getProtocolVersion()
61+
{
62+
return $this->protocolVersion;
63+
}
64+
65+
public function withProtocolVersion($version)
66+
{
67+
if ((string) $version === $this->protocolVersion) {
68+
return $this;
69+
}
70+
71+
$message = clone $this;
72+
$message->protocolVersion = (string) $version;
73+
74+
return $message;
75+
}
76+
77+
public function getHeaders()
78+
{
79+
return $this->headers;
80+
}
81+
82+
public function hasHeader($name)
83+
{
84+
return isset($this->headerNamesLowerCase[\strtolower($name)]);
85+
}
86+
87+
public function getHeader($name)
88+
{
89+
$lower = \strtolower($name);
90+
return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array();
91+
}
92+
93+
public function getHeaderLine($name)
94+
{
95+
return \implode(', ', $this->getHeader($name));
96+
}
97+
98+
public function withHeader($name, $value)
99+
{
100+
if ($value === array()) {
101+
return $this->withoutHeader($name);
102+
} elseif (\is_array($value)) {
103+
foreach ($value as &$one) {
104+
$one = (string) $one;
105+
}
106+
} else {
107+
$value = array((string) $value);
108+
}
109+
110+
$lower = \strtolower($name);
111+
if (isset($this->headerNamesLowerCase[$lower]) && $this->headerNamesLowerCase[$lower] === (string) $name && $this->headers[$this->headerNamesLowerCase[$lower]] === $value) {
112+
return $this;
113+
}
114+
115+
$message = clone $this;
116+
if (isset($message->headerNamesLowerCase[$lower])) {
117+
unset($message->headers[$message->headerNamesLowerCase[$lower]]);
118+
}
119+
120+
$message->headers[$name] = $value;
121+
$message->headerNamesLowerCase[$lower] = $name;
122+
123+
return $message;
124+
}
125+
126+
public function withAddedHeader($name, $value)
127+
{
128+
if ($value === array()) {
129+
return $this;
130+
}
131+
132+
return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value)));
133+
}
134+
135+
public function withoutHeader($name)
136+
{
137+
$lower = \strtolower($name);
138+
if (!isset($this->headerNamesLowerCase[$lower])) {
139+
return $this;
140+
}
141+
142+
$message = clone $this;
143+
unset($message->headers[$message->headerNamesLowerCase[$lower]], $message->headerNamesLowerCase[$lower]);
144+
145+
return $message;
146+
}
147+
148+
public function getBody()
149+
{
150+
return $this->body;
151+
}
152+
153+
public function withBody(StreamInterface $body)
154+
{
155+
if ($body === $this->body) {
156+
return $this;
157+
}
158+
159+
$message = clone $this;
160+
$message->body = $body;
161+
162+
return $message;
163+
}
164+
}

src/Message/Response.php

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
namespace React\Http\Message;
44

55
use Fig\Http\Message\StatusCodeInterface;
6+
use Psr\Http\Message\ResponseInterface;
67
use Psr\Http\Message\StreamInterface;
8+
use React\Http\Io\AbstractMessage;
79
use React\Http\Io\BufferedBody;
810
use React\Http\Io\HttpBodyStream;
911
use React\Stream\ReadableStreamInterface;
10-
use RingCentral\Psr7\Response as Psr7Response;
1112

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

260+
/**
261+
* @var bool
262+
* @see self::$phrasesMap
263+
*/
264+
private static $phrasesInitialized = false;
265+
266+
/**
267+
* Map of standard HTTP status codes to standard reason phrases.
268+
*
269+
* This map will be fully populated with all standard reason phrases on
270+
* first access. By default, it only contains a subset of HTTP status codes
271+
* that have a custom mapping to reason phrases (such as those with dashes
272+
* and all caps words). See `self::STATUS_*` for all possible status code
273+
* constants.
274+
*
275+
* @var array<int,string>
276+
* @see self::STATUS_*
277+
* @see self::getReasonPhraseForStatusCode()
278+
*/
279+
private static $phrasesMap = array(
280+
200 => 'OK',
281+
203 => 'Non-Authoritative Information',
282+
207 => 'Multi-Status',
283+
226 => 'IM Used',
284+
414 => 'URI Too Large',
285+
418 => 'I\'m a teapot',
286+
505 => 'HTTP Version Not Supported'
287+
);
288+
289+
/** @var int */
290+
private $statusCode;
291+
292+
/** @var string */
293+
private $reasonPhrase;
294+
260295
/**
261296
* @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants
262297
* @param array<string,string|string[]> $headers additional response headers
@@ -280,12 +315,58 @@ public function __construct(
280315
throw new \InvalidArgumentException('Invalid response body given');
281316
}
282317

283-
parent::__construct(
284-
$status,
285-
$headers,
286-
$body,
287-
$version,
288-
$reason
289-
);
318+
parent::__construct($version, $headers, $body);
319+
320+
$this->statusCode = (int) $status;
321+
$this->reasonPhrase = ($reason !== '' && $reason !== null) ? (string) $reason : self::getReasonPhraseForStatusCode($status);
322+
}
323+
324+
public function getStatusCode()
325+
{
326+
return $this->statusCode;
327+
}
328+
329+
public function withStatus($code, $reasonPhrase = '')
330+
{
331+
if ((string) $reasonPhrase === '') {
332+
$reasonPhrase = self::getReasonPhraseForStatusCode($code);
333+
}
334+
335+
if ($this->statusCode === (int) $code && $this->reasonPhrase === (string) $reasonPhrase) {
336+
return $this;
337+
}
338+
339+
$response = clone $this;
340+
$response->statusCode = (int) $code;
341+
$response->reasonPhrase = (string) $reasonPhrase;
342+
343+
return $response;
344+
}
345+
346+
public function getReasonPhrase()
347+
{
348+
return $this->reasonPhrase;
349+
}
350+
351+
/**
352+
* @param int $code
353+
* @return string default reason phrase for given status code or empty string if unknown
354+
*/
355+
private static function getReasonPhraseForStatusCode($code)
356+
{
357+
if (!self::$phrasesInitialized) {
358+
self::$phrasesInitialized = true;
359+
360+
// map all `self::STATUS_` constants from status code to reason phrase
361+
// e.g. `self::STATUS_NOT_FOUND = 404` will be mapped to `404 Not Found`
362+
$ref = new \ReflectionClass(__CLASS__);
363+
foreach ($ref->getConstants() as $name => $value) {
364+
if (!isset(self::$phrasesMap[$value]) && \strpos($name, 'STATUS_') === 0) {
365+
self::$phrasesMap[$value] = \ucwords(\strtolower(\str_replace('_', ' ', \substr($name, 7))));
366+
}
367+
}
368+
}
369+
370+
return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : '';
290371
}
291372
}

0 commit comments

Comments
 (0)