Skip to content

Commit b6ebd76

Browse files
committed
[1.x] Support PSR-7 v2 along side v1
This changeset introduces our PSR-7 v2 implementation alongside our PSR-7 v1 implementation and goes with v1 or v2 depending on which `psr/http-message` version is installed. This is admittedly not my best code, nor am I proud of it, but it gets the job done. This relies on clue/reactphp-http-proxy#65 being merged first. Implements #513 for `react/http` v1.
1 parent b489e05 commit b6ebd76

26 files changed

Lines changed: 2420 additions & 9 deletions

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jobs:
99
name: PHPUnit (PHP ${{ matrix.php }})
1010
runs-on: ubuntu-24.04
1111
strategy:
12+
fail-fast: false
1213
matrix:
1314
php:
1415
- 8.4

‎composer.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@
2929
"php": ">=5.3.0",
3030
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
3131
"fig/http-message-util": "^1.1",
32-
"psr/http-message": "^1.0",
32+
"psr/http-message": "^2.0 || ^1.0",
3333
"react/event-loop": "^1.2",
3434
"react/promise": "^3.2 || ^2.3 || ^1.2.1",
3535
"react/socket": "^1.16",
3636
"react/stream": "^1.4"
3737
},
3838
"require-dev": {
39-
"clue/http-proxy-react": "^1.8",
39+
"clue/http-proxy-react": "^1.10",
4040
"clue/reactphp-ssh-proxy": "^1.4",
4141
"clue/socks-react": "^1.4",
42-
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
42+
"phpunit/phpunit": "^9.6 || ^8.5 || ^5.7 || ^4.8.36",
4343
"react/async": "^4.2 || ^3 || ^2",
4444
"react/promise-stream": "^1.4",
4545
"react/promise-timer": "^1.11"

‎src/Io/AbstractMessage.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use Psr\Http\Message\MessageInterface;
66
use Psr\Http\Message\StreamInterface;
77

8+
$reflectedMethod = new \ReflectionMethod('\Psr\Http\Message\MessageInterface','withProtocolVersion');
9+
if (!(PHP_VERSION_ID >= 70000 && $reflectedMethod->hasReturnType())) {
810
/**
911
* [Internal] Abstract HTTP message base class (PSR-7)
1012
*
@@ -170,3 +172,6 @@ public function withBody(StreamInterface $body)
170172
return $message;
171173
}
172174
}
175+
} else {
176+
class_alias(__NAMESPACE__ . '\\V2\\AbstractMessage', __NAMESPACE__ . '\\AbstractMessage');
177+
}

‎src/Io/AbstractRequest.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Psr\Http\Message\UriInterface;
88
use React\Http\Message\Uri;
99

10+
$reflectedMethod = new \ReflectionMethod('\Psr\Http\Message\RequestInterface','withRequestTarget');
11+
if (!(PHP_VERSION_ID >= 70000 && $reflectedMethod->hasReturnType())) {
1012
/**
1113
* [Internal] Abstract HTTP request base class (PSR-7)
1214
*
@@ -154,3 +156,6 @@ public function withUri(UriInterface $uri, $preserveHost = false)
154156
return $request;
155157
}
156158
}
159+
} else {
160+
class_alias(__NAMESPACE__ . '\\V2\\AbstractRequest', __NAMESPACE__ . '\\AbstractRequest');
161+
}

‎src/Io/BufferedBody.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use Psr\Http\Message\StreamInterface;
66

7+
$reflectedMethod = new \ReflectionMethod('\Psr\Http\Message\StreamInterface','eof');
8+
if (!(PHP_VERSION_ID >= 70000 && $reflectedMethod->hasReturnType())) {
79
/**
810
* [Internal] PSR-7 message body implementation using an in-memory buffer
911
*
@@ -177,3 +179,6 @@ public function getMetadata($key = null)
177179
return $key === null ? array() : null;
178180
}
179181
}
182+
} else {
183+
class_alias(__NAMESPACE__ . '\\V2\\BufferedBody', __NAMESPACE__ . '\\BufferedBody');
184+
}

‎src/Io/EmptyBodyStream.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use React\Stream\Util;
99
use React\Stream\WritableStreamInterface;
1010

11+
$reflectedMethod = new \ReflectionMethod('\Psr\Http\Message\StreamInterface','eof');
12+
if (!(PHP_VERSION_ID >= 70000 && $reflectedMethod->hasReturnType())) {
1113
/**
1214
* [Internal] Bridge between an empty StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP
1315
*
@@ -140,3 +142,6 @@ public function getMetadata($key = null)
140142
return ($key === null) ? array() : null;
141143
}
142144
}
145+
} else {
146+
class_alias(__NAMESPACE__ . '\\V2\\EmptyBodyStream', __NAMESPACE__ . '\\EmptyBodyStream');
147+
}

‎src/Io/HttpBodyStream.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use React\Stream\Util;
99
use React\Stream\WritableStreamInterface;
1010

11+
$reflectedMethod = new \ReflectionMethod('\Psr\Http\Message\StreamInterface','eof');
12+
if (!(PHP_VERSION_ID >= 70000 && $reflectedMethod->hasReturnType())) {
1113
/**
1214
* [Internal] Bridge between StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP
1315
*
@@ -180,3 +182,6 @@ public function handleEnd()
180182
}
181183
}
182184
}
185+
} else {
186+
class_alias(__NAMESPACE__ . '\\V2\\HttpBodyStream', __NAMESPACE__ . '\\HttpBodyStream');
187+
}

‎src/Io/ReadableBodyStream.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use React\Stream\Util;
99
use React\Stream\WritableStreamInterface;
1010

11+
$reflectedMethod = new \ReflectionMethod('\Psr\Http\Message\StreamInterface','eof');
12+
if (!(PHP_VERSION_ID >= 70000 && $reflectedMethod->hasReturnType())) {
1113
/**
1214
* @internal
1315
*/
@@ -151,3 +153,6 @@ public function handleEnd()
151153
$this->close();
152154
}
153155
}
156+
} else {
157+
class_alias(__NAMESPACE__ . '\\V2\\ReadableBodyStream', __NAMESPACE__ . '\\ReadableBodyStream');
158+
}

‎src/Io/UploadedFile.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use InvalidArgumentException;
88
use RuntimeException;
99

10+
$reflectedMethod = new \ReflectionMethod('\Psr\Http\Message\UploadedFileInterface','getStream');
11+
if (!(PHP_VERSION_ID >= 70000 && $reflectedMethod->hasReturnType())) {
1012
/**
1113
* [Internal] Implementation of the PSR-7 `UploadedFileInterface`
1214
*
@@ -18,7 +20,7 @@
1820
* @see UploadedFileInterface
1921
* @internal
2022
*/
21-
final class UploadedFile implements UploadedFileInterface
23+
class UploadedFile implements UploadedFileInterface
2224
{
2325
/**
2426
* @var StreamInterface
@@ -128,3 +130,6 @@ public function getClientMediaType()
128130
return $this->mediaType;
129131
}
130132
}
133+
} else {
134+
class_alias(__NAMESPACE__ . '\\V2\\UploadedFile', __NAMESPACE__ . '\\UploadedFile');
135+
}

‎src/Io/V2/AbstractMessage.php‎

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
namespace React\Http\Io\V2;
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+
* @deprecated Do not reference this FQCN directly. Not a runtime deprecation:
15+
* when psr/http-message v2 is installed this class is the
16+
* implementation behind the public name. Marked deprecated so
17+
* tooling suggests the aliased class instead (see sibling file).
18+
* @see \React\Http\Io\AbstractMessage
19+
* @see ../AbstractMessage.php
20+
*/
21+
abstract class AbstractMessage implements MessageInterface
22+
{
23+
/**
24+
* [Internal] Regex used to match all request header fields into an array, thanks to @kelunik for checking the HTTP specs and coming up with this regex
25+
*
26+
* @internal
27+
* @var string
28+
*/
29+
const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x00-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m';
30+
31+
/** @var array<string,string[]> */
32+
private $headers = array();
33+
34+
/** @var array<string,string> */
35+
private $headerNamesLowerCase = array();
36+
37+
/** @var string */
38+
private $protocolVersion;
39+
40+
/** @var StreamInterface */
41+
private $body;
42+
43+
/**
44+
* @param string $protocolVersion
45+
* @param array<string,string|string[]> $headers
46+
* @param StreamInterface $body
47+
*/
48+
protected function __construct($protocolVersion, array $headers, StreamInterface $body)
49+
{
50+
foreach ($headers as $name => $value) {
51+
if ($value !== array()) {
52+
if (\is_array($value)) {
53+
foreach ($value as &$one) {
54+
$one = (string) $one;
55+
}
56+
} else {
57+
$value = array((string) $value);
58+
}
59+
60+
$lower = \strtolower($name);
61+
if (isset($this->headerNamesLowerCase[$lower])) {
62+
$value = \array_merge($this->headers[$this->headerNamesLowerCase[$lower]], $value);
63+
unset($this->headers[$this->headerNamesLowerCase[$lower]]);
64+
}
65+
66+
$this->headers[$name] = $value;
67+
$this->headerNamesLowerCase[$lower] = $name;
68+
}
69+
}
70+
71+
$this->protocolVersion = (string) $protocolVersion;
72+
$this->body = $body;
73+
}
74+
75+
public function getProtocolVersion(): string
76+
{
77+
return $this->protocolVersion;
78+
}
79+
80+
public function withProtocolVersion(string $version): MessageInterface
81+
{
82+
if ((string) $version === $this->protocolVersion) {
83+
return $this;
84+
}
85+
86+
$message = clone $this;
87+
$message->protocolVersion = (string) $version;
88+
89+
return $message;
90+
}
91+
92+
public function getHeaders(): array
93+
{
94+
return $this->headers;
95+
}
96+
97+
public function hasHeader(string $name): bool
98+
{
99+
return isset($this->headerNamesLowerCase[\strtolower($name)]);
100+
}
101+
102+
public function getHeader(string $name): array
103+
{
104+
$lower = \strtolower($name);
105+
return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array();
106+
}
107+
108+
public function getHeaderLine(string $name): string
109+
{
110+
return \implode(', ', $this->getHeader($name));
111+
}
112+
113+
public function withHeader(string $name, $value): MessageInterface
114+
{
115+
if ($value === array()) {
116+
return $this->withoutHeader($name);
117+
} elseif (\is_array($value)) {
118+
foreach ($value as &$one) {
119+
$one = (string) $one;
120+
}
121+
} else {
122+
$value = array((string) $value);
123+
}
124+
125+
$lower = \strtolower($name);
126+
if (isset($this->headerNamesLowerCase[$lower]) && $this->headerNamesLowerCase[$lower] === (string) $name && $this->headers[$this->headerNamesLowerCase[$lower]] === $value) {
127+
return $this;
128+
}
129+
130+
$message = clone $this;
131+
if (isset($message->headerNamesLowerCase[$lower])) {
132+
unset($message->headers[$message->headerNamesLowerCase[$lower]]);
133+
}
134+
135+
$message->headers[$name] = $value;
136+
$message->headerNamesLowerCase[$lower] = $name;
137+
138+
return $message;
139+
}
140+
141+
public function withAddedHeader(string $name, $value): MessageInterface
142+
{
143+
if ($value === array()) {
144+
return $this;
145+
}
146+
147+
return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value)));
148+
}
149+
150+
public function withoutHeader(string $name): MessageInterface
151+
{
152+
$lower = \strtolower($name);
153+
if (!isset($this->headerNamesLowerCase[$lower])) {
154+
return $this;
155+
}
156+
157+
$message = clone $this;
158+
unset($message->headers[$message->headerNamesLowerCase[$lower]], $message->headerNamesLowerCase[$lower]);
159+
160+
return $message;
161+
}
162+
163+
public function getBody(): StreamInterface
164+
{
165+
return $this->body;
166+
}
167+
168+
public function withBody(StreamInterface $body): MessageInterface
169+
{
170+
if ($body === $this->body) {
171+
return $this;
172+
}
173+
174+
$message = clone $this;
175+
$message->body = $body;
176+
177+
return $message;
178+
}
179+
}

0 commit comments

Comments
 (0)