Skip to content

Commit 4b35cc2

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 4b35cc2

30 files changed

Lines changed: 3542 additions & 1128 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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace React\Http\Io;
44

5+
use Composer\InstalledVersions;
56
use Psr\Http\Message\MessageInterface;
67
use Psr\Http\Message\StreamInterface;
78

9+
if (version_compare(InstalledVersions::getVersion('psr/http-message'), '2.0.0', '<')) {
810
/**
911
* [Internal] Abstract HTTP message base class (PSR-7)
1012
*
@@ -170,3 +172,8 @@ public function withBody(StreamInterface $body)
170172
return $message;
171173
}
172174
}
175+
} else {
176+
abstract class AbstractMessage extends V2\AbstractMessage
177+
{
178+
}
179+
}

src/Io/AbstractRequest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
namespace React\Http\Io;
44

5+
use Composer\InstalledVersions;
56
use Psr\Http\Message\RequestInterface;
67
use Psr\Http\Message\StreamInterface;
78
use Psr\Http\Message\UriInterface;
89
use React\Http\Message\Uri;
910

11+
if (version_compare(InstalledVersions::getVersion('psr/http-message'), '2.0.0', '<')) {
1012
/**
1113
* [Internal] Abstract HTTP request base class (PSR-7)
1214
*
@@ -154,3 +156,8 @@ public function withUri(UriInterface $uri, $preserveHost = false)
154156
return $request;
155157
}
156158
}
159+
} else {
160+
abstract class AbstractRequest extends V2\AbstractRequest
161+
{
162+
}
163+
}

src/Io/BufferedBody.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
namespace React\Http\Io;
44

5+
use Composer\InstalledVersions;
56
use Psr\Http\Message\StreamInterface;
67

8+
if (version_compare(InstalledVersions::getVersion('psr/http-message'), '2.0.0', '<')) {
79
/**
810
* [Internal] PSR-7 message body implementation using an in-memory buffer
911
*
1012
* @internal
1113
*/
12-
class BufferedBody implements StreamInterface
14+
abstract class BufferedBody implements StreamInterface
1315
{
1416
private $buffer = '';
1517
private $position = 0;
@@ -177,3 +179,8 @@ public function getMetadata($key = null)
177179
return $key === null ? array() : null;
178180
}
179181
}
182+
} else {
183+
class BufferedBody extends V2\BufferedBody
184+
{
185+
}
186+
}

src/Io/EmptyBodyStream.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace React\Http\Io;
44

5+
use Composer\InstalledVersions;
56
use Evenement\EventEmitter;
67
use Psr\Http\Message\StreamInterface;
78
use React\Stream\ReadableStreamInterface;
89
use React\Stream\Util;
910
use React\Stream\WritableStreamInterface;
1011

12+
if (version_compare(InstalledVersions::getVersion('psr/http-message'), '2.0.0', '<')) {
1113
/**
1214
* [Internal] Bridge between an empty StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP
1315
*
@@ -140,3 +142,8 @@ public function getMetadata($key = null)
140142
return ($key === null) ? array() : null;
141143
}
142144
}
145+
} else {
146+
class EmptyBodyStream extends V2\EmptyBodyStream
147+
{
148+
}
149+
}

src/Io/HttpBodyStream.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace React\Http\Io;
44

5+
use Composer\InstalledVersions;
56
use Evenement\EventEmitter;
67
use Psr\Http\Message\StreamInterface;
78
use React\Stream\ReadableStreamInterface;
89
use React\Stream\Util;
910
use React\Stream\WritableStreamInterface;
1011

12+
if (version_compare(InstalledVersions::getVersion('psr/http-message'), '2.0.0', '<')) {
1113
/**
1214
* [Internal] Bridge between StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP
1315
*
@@ -24,7 +26,7 @@
2426
* @see ReadableStreamInterface
2527
* @internal
2628
*/
27-
class HttpBodyStream extends EventEmitter implements StreamInterface, ReadableStreamInterface
29+
abstract class HttpBodyStream extends EventEmitter implements StreamInterface, ReadableStreamInterface
2830
{
2931
public $input;
3032
private $closed = false;
@@ -180,3 +182,8 @@ public function handleEnd()
180182
}
181183
}
182184
}
185+
} else {
186+
class HttpBodyStream extends V2\HttpBodyStream
187+
{
188+
}
189+
}

src/Io/ReadableBodyStream.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace React\Http\Io;
44

5+
use Composer\InstalledVersions;
56
use Evenement\EventEmitter;
67
use Psr\Http\Message\StreamInterface;
78
use React\Stream\ReadableStreamInterface;
89
use React\Stream\Util;
910
use React\Stream\WritableStreamInterface;
1011

12+
if (version_compare(InstalledVersions::getVersion('psr/http-message'), '2.0.0', '<')) {
1113
/**
1214
* @internal
1315
*/
@@ -151,3 +153,8 @@ public function handleEnd()
151153
$this->close();
152154
}
153155
}
156+
} else {
157+
class ReadableBodyStream extends V2\ReadableBodyStream
158+
{
159+
}
160+
}

src/Io/UploadedFile.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
namespace React\Http\Io;
44

5+
use Composer\InstalledVersions;
56
use Psr\Http\Message\StreamInterface;
67
use Psr\Http\Message\UploadedFileInterface;
78
use InvalidArgumentException;
89
use RuntimeException;
910

11+
if (version_compare(InstalledVersions::getVersion('psr/http-message'), '2.0.0', '<')) {
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+
abstract class UploadedFile implements UploadedFileInterface
2224
{
2325
/**
2426
* @var StreamInterface
@@ -128,3 +130,8 @@ public function getClientMediaType()
128130
return $this->mediaType;
129131
}
130132
}
133+
} else {
134+
final class UploadedFile extends V2\UploadedFile
135+
{
136+
}
137+
}

src/Io/V2/AbstractMessage.php

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

0 commit comments

Comments
 (0)