Skip to content

Commit d199fd9

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 d199fd9

37 files changed

Lines changed: 4638 additions & 2169 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: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,16 @@
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",
4039
"clue/reactphp-ssh-proxy": "^1.4",
4140
"clue/socks-react": "^1.4",
42-
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
41+
"phpunit/phpunit": "^9.6.34 || 5.7.27 || ^4.8.36",
4342
"react/async": "^4.2 || ^3 || ^2",
4443
"react/promise-stream": "^1.4",
4544
"react/promise-timer": "^1.11"

src/Io/AbstractMessage.php

Lines changed: 5 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -2,171 +2,14 @@
22

33
namespace React\Http\Io;
44

5-
use Psr\Http\Message\MessageInterface;
6-
use Psr\Http\Message\StreamInterface;
5+
use Composer\InstalledVersions;
76

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()
69-
{
70-
return $this->protocolVersion;
71-
}
72-
73-
public function withProtocolVersion($version)
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()
86-
{
87-
return $this->headers;
88-
}
89-
90-
public function hasHeader($name)
91-
{
92-
return isset($this->headerNamesLowerCase[\strtolower($name)]);
93-
}
94-
95-
public function getHeader($name)
7+
if (version_compare(InstalledVersions::getVersion('psr/http-message'), '2.0.0', '<')) {
8+
abstract class AbstractMessage extends V1\AbstractMessage
969
{
97-
$lower = \strtolower($name);
98-
return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array();
9910
}
100-
101-
public function getHeaderLine($name)
102-
{
103-
return \implode(', ', $this->getHeader($name));
104-
}
105-
106-
public function withHeader($name, $value)
11+
} else {
12+
abstract class AbstractMessage extends V2\AbstractMessage
10713
{
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($name, $value)
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($name)
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()
157-
{
158-
return $this->body;
159-
}
160-
161-
public function withBody(StreamInterface $body)
162-
{
163-
if ($body === $this->body) {
164-
return $this;
165-
}
166-
167-
$message = clone $this;
168-
$message->body = $body;
169-
170-
return $message;
17114
}
17215
}

src/Io/AbstractRequest.php

Lines changed: 5 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -2,155 +2,14 @@
22

33
namespace React\Http\Io;
44

5-
use Psr\Http\Message\RequestInterface;
6-
use Psr\Http\Message\StreamInterface;
7-
use Psr\Http\Message\UriInterface;
8-
use React\Http\Message\Uri;
5+
use Composer\InstalledVersions;
96

10-
/**
11-
* [Internal] Abstract HTTP request base class (PSR-7)
12-
*
13-
* @internal
14-
* @see RequestInterface
15-
*/
16-
abstract class AbstractRequest extends AbstractMessage implements RequestInterface
17-
{
18-
/** @var ?string */
19-
private $requestTarget;
20-
21-
/** @var string */
22-
private $method;
23-
24-
/** @var UriInterface */
25-
private $uri;
26-
27-
/**
28-
* @param string $method
29-
* @param string|UriInterface $uri
30-
* @param array<string,string|string[]> $headers
31-
* @param StreamInterface $body
32-
* @param string unknown $protocolVersion
33-
*/
34-
protected function __construct(
35-
$method,
36-
$uri,
37-
array $headers,
38-
StreamInterface $body,
39-
$protocolVersion
40-
) {
41-
if (\is_string($uri)) {
42-
$uri = new Uri($uri);
43-
} elseif (!$uri instanceof UriInterface) {
44-
throw new \InvalidArgumentException(
45-
'Argument #2 ($uri) expected string|Psr\Http\Message\UriInterface'
46-
);
47-
}
48-
49-
// assign default `Host` request header from URI unless already given explicitly
50-
$host = $uri->getHost();
51-
if ($host !== '') {
52-
foreach ($headers as $name => $value) {
53-
if (\strtolower($name) === 'host' && $value !== array()) {
54-
$host = '';
55-
break;
56-
}
57-
}
58-
if ($host !== '') {
59-
$port = $uri->getPort();
60-
if ($port !== null && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) {
61-
$host .= ':' . $port;
62-
}
63-
64-
$headers = array('Host' => $host) + $headers;
65-
}
66-
}
67-
68-
parent::__construct($protocolVersion, $headers, $body);
69-
70-
$this->method = $method;
71-
$this->uri = $uri;
72-
}
73-
74-
public function getRequestTarget()
7+
if (version_compare(InstalledVersions::getVersion('psr/http-message'), '2.0.0', '<')) {
8+
abstract class AbstractRequest extends V1\AbstractRequest
759
{
76-
if ($this->requestTarget !== null) {
77-
return $this->requestTarget;
78-
}
79-
80-
$target = $this->uri->getPath();
81-
if ($target === '') {
82-
$target = '/';
83-
}
84-
if (($query = $this->uri->getQuery()) !== '') {
85-
$target .= '?' . $query;
86-
}
87-
88-
return $target;
89-
}
90-
91-
public function withRequestTarget($requestTarget)
92-
{
93-
if ((string) $requestTarget === $this->requestTarget) {
94-
return $this;
95-
}
96-
97-
$request = clone $this;
98-
$request->requestTarget = (string) $requestTarget;
99-
100-
return $request;
10110
}
102-
103-
public function getMethod()
11+
} else {
12+
abstract class AbstractRequest extends V2\AbstractRequest
10413
{
105-
return $this->method;
106-
}
107-
108-
public function withMethod($method)
109-
{
110-
if ((string) $method === $this->method) {
111-
return $this;
112-
}
113-
114-
$request = clone $this;
115-
$request->method = (string) $method;
116-
117-
return $request;
118-
}
119-
120-
public function getUri()
121-
{
122-
return $this->uri;
123-
}
124-
125-
public function withUri(UriInterface $uri, $preserveHost = false)
126-
{
127-
if ($uri === $this->uri) {
128-
return $this;
129-
}
130-
131-
$request = clone $this;
132-
$request->uri = $uri;
133-
134-
$host = $uri->getHost();
135-
$port = $uri->getPort();
136-
if ($port !== null && $host !== '' && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) {
137-
$host .= ':' . $port;
138-
}
139-
140-
// update `Host` request header if URI contains a new host and `$preserveHost` is false
141-
if ($host !== '' && (!$preserveHost || $request->getHeaderLine('Host') === '')) {
142-
// first remove all headers before assigning `Host` header to ensure it always comes first
143-
foreach (\array_keys($request->getHeaders()) as $name) {
144-
$request = $request->withoutHeader($name);
145-
}
146-
147-
// add `Host` header first, then all other original headers
148-
$request = $request->withHeader('Host', $host);
149-
foreach ($this->withoutHeader('Host')->getHeaders() as $name => $value) {
150-
$request = $request->withHeader($name, $value);
151-
}
152-
}
153-
154-
return $request;
15514
}
15615
}

0 commit comments

Comments
 (0)