Skip to content

Commit a73e9f7

Browse files
committed
Add new Uri class for new PSR-7 implementation
1 parent 0638dcd commit a73e9f7

File tree

8 files changed

+890
-5
lines changed

8 files changed

+890
-5
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ multiple concurrent HTTP requests without blocking.
7979
* [xml()](#xml)
8080
* [Request](#request-1)
8181
* [ServerRequest](#serverrequest)
82+
* [Uri](#uri)
8283
* [ResponseException](#responseexception)
8384
* [React\Http\Middleware](#reacthttpmiddleware)
8485
* [StreamingRequestMiddleware](#streamingrequestmiddleware)
@@ -2664,6 +2665,18 @@ application reacts to certain HTTP requests.
26642665
> Internally, this implementation builds on top of a base class which is
26652666
considered an implementation detail that may change in the future.
26662667

2668+
#### Uri
2669+
2670+
The `React\Http\Message\Uri` class can be used to
2671+
respresent a URI (or URL).
2672+
2673+
This class implements the
2674+
[PSR-7 `UriInterface`](https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface).
2675+
2676+
This is mostly used internally to represent the URI of each HTTP request
2677+
message for our HTTP client and server implementations. Likewise, you may
2678+
also use this class with other HTTP implementations and for tests.
2679+
26672680
#### ResponseException
26682681

26692682
The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject

src/Io/AbstractRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Psr\Http\Message\RequestInterface;
66
use Psr\Http\Message\StreamInterface;
77
use Psr\Http\Message\UriInterface;
8-
use RingCentral\Psr7\Uri;
8+
use React\Http\Message\Uri;
99

1010
/**
1111
* [Internal] Abstract HTTP request base class (PSR-7)

src/Message/Uri.php

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
<?php
2+
3+
namespace React\Http\Message;
4+
5+
use Psr\Http\Message\UriInterface;
6+
7+
/**
8+
* Respresents a URI (or URL).
9+
*
10+
* This class implements the
11+
* [PSR-7 `UriInterface`](https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface).
12+
*
13+
* This is mostly used internally to represent the URI of each HTTP request
14+
* message for our HTTP client and server implementations. Likewise, you may
15+
* also use this class with other HTTP implementations and for tests.
16+
*
17+
* @see UriInterface
18+
*/
19+
final class Uri implements UriInterface
20+
{
21+
/** @var string */
22+
private $scheme = '';
23+
24+
/** @var string */
25+
private $userInfo = '';
26+
27+
/** @var string */
28+
private $host = '';
29+
30+
/** @var ?int */
31+
private $port = null;
32+
33+
/** @var string */
34+
private $path = '';
35+
36+
/** @var string */
37+
private $query = '';
38+
39+
/** @var string */
40+
private $fragment = '';
41+
42+
/**
43+
* @param string $uri
44+
* @throws \InvalidArgumentException if given $uri is invalid
45+
*/
46+
public function __construct($uri)
47+
{
48+
// @codeCoverageIgnoreStart
49+
if (\PHP_VERSION_ID < 50407 && \strpos($uri, '//') === 0) {
50+
// @link https://3v4l.org/UrAQP
51+
$parts = \parse_url('http:' . $uri);
52+
unset($parts['schema']);
53+
} else {
54+
$parts = \parse_url($uri);
55+
}
56+
// @codeCoverageIgnoreEnd
57+
58+
if ($parts === false || (isset($parts['scheme']) && !\preg_match('#^[a-z]+$#i', $parts['scheme'])) || (isset($parts['host']) && \preg_match('#[\s_%+]#', $parts['host']))) {
59+
throw new \InvalidArgumentException('Invalid URI given');
60+
}
61+
62+
if (isset($parts['scheme'])) {
63+
$this->scheme = \strtolower($parts['scheme']);
64+
}
65+
66+
if (isset($parts['user']) || isset($parts['pass'])) {
67+
$this->userInfo = $this->encode(isset($parts['user']) ? $parts['user'] : '', \PHP_URL_USER) . (isset($parts['pass']) ? ':' . $this->encode($parts['pass'], \PHP_URL_PASS) : '');
68+
}
69+
70+
if (isset($parts['host'])) {
71+
$this->host = \strtolower($parts['host']);
72+
}
73+
74+
if (isset($parts['port']) && !(($parts['port'] === 80 && $this->scheme === 'http') || ($parts['port'] === 443 && $this->scheme === 'https'))) {
75+
$this->port = $parts['port'];
76+
}
77+
78+
if (isset($parts['path'])) {
79+
$this->path = $this->encode($parts['path'], \PHP_URL_PATH);
80+
}
81+
82+
if (isset($parts['query'])) {
83+
$this->query = $this->encode($parts['query'], \PHP_URL_QUERY);
84+
}
85+
86+
if (isset($parts['fragment'])) {
87+
$this->fragment = $this->encode($parts['fragment'], \PHP_URL_FRAGMENT);
88+
}
89+
}
90+
91+
public function getScheme()
92+
{
93+
return $this->scheme;
94+
}
95+
96+
public function getAuthority()
97+
{
98+
if ($this->host === '') {
99+
return '';
100+
}
101+
102+
return ($this->userInfo !== '' ? $this->userInfo . '@' : '') . $this->host . ($this->port !== null ? ':' . $this->port : '');
103+
}
104+
105+
public function getUserInfo()
106+
{
107+
return $this->userInfo;
108+
}
109+
110+
public function getHost()
111+
{
112+
return $this->host;
113+
}
114+
115+
public function getPort()
116+
{
117+
return $this->port;
118+
}
119+
120+
public function getPath()
121+
{
122+
return $this->path;
123+
}
124+
125+
public function getQuery()
126+
{
127+
return $this->query;
128+
}
129+
130+
public function getFragment()
131+
{
132+
return $this->fragment;
133+
}
134+
135+
public function withScheme($scheme)
136+
{
137+
$scheme = \strtolower($scheme);
138+
if ($scheme === $this->scheme) {
139+
return $this;
140+
}
141+
142+
if (!\preg_match('#^[a-z]*$#', $scheme)) {
143+
throw new \InvalidArgumentException('Invalid URI scheme given');
144+
}
145+
146+
$new = clone $this;
147+
$new->scheme = $scheme;
148+
149+
if (($this->port === 80 && $scheme === 'http') || ($this->port === 443 && $scheme === 'https')) {
150+
$new->port = null;
151+
}
152+
153+
return $new;
154+
}
155+
156+
public function withUserInfo($user, $password = null)
157+
{
158+
$userInfo = $this->encode($user, \PHP_URL_USER) . ($password !== null ? ':' . $this->encode($password, \PHP_URL_PASS) : '');
159+
if ($userInfo === $this->userInfo) {
160+
return $this;
161+
}
162+
163+
$new = clone $this;
164+
$new->userInfo = $userInfo;
165+
166+
return $new;
167+
}
168+
169+
public function withHost($host)
170+
{
171+
$host = \strtolower($host);
172+
if ($host === $this->host) {
173+
return $this;
174+
}
175+
176+
if (\preg_match('#[\s_%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) {
177+
throw new \InvalidArgumentException('Invalid URI host given');
178+
}
179+
180+
$new = clone $this;
181+
$new->host = $host;
182+
183+
return $new;
184+
}
185+
186+
public function withPort($port)
187+
{
188+
$port = $port === null ? null : (int) $port;
189+
if (($port === 80 && $this->scheme === 'http') || ($port === 443 && $this->scheme === 'https')) {
190+
$port = null;
191+
}
192+
193+
if ($port === $this->port) {
194+
return $this;
195+
}
196+
197+
if ($port !== null && ($port < 1 || $port > 0xffff)) {
198+
throw new \InvalidArgumentException('Invalid URI port given');
199+
}
200+
201+
$new = clone $this;
202+
$new->port = $port;
203+
204+
return $new;
205+
}
206+
207+
public function withPath($path)
208+
{
209+
$path = $this->encode($path, \PHP_URL_PATH);
210+
if ($path === $this->path) {
211+
return $this;
212+
}
213+
214+
$new = clone $this;
215+
$new->path = $path;
216+
217+
return $new;
218+
}
219+
220+
public function withQuery($query)
221+
{
222+
$query = $this->encode($query, \PHP_URL_QUERY);
223+
if ($query === $this->query) {
224+
return $this;
225+
}
226+
227+
$new = clone $this;
228+
$new->query = $query;
229+
230+
return $new;
231+
}
232+
233+
public function withFragment($fragment)
234+
{
235+
$fragment = $this->encode($fragment, \PHP_URL_FRAGMENT);
236+
if ($fragment === $this->fragment) {
237+
return $this;
238+
}
239+
240+
$new = clone $this;
241+
$new->fragment = $fragment;
242+
243+
return $new;
244+
}
245+
246+
public function __toString()
247+
{
248+
$uri = '';
249+
if ($this->scheme !== '') {
250+
$uri .= $this->scheme . ':';
251+
}
252+
253+
$authority = $this->getAuthority();
254+
if ($authority !== '') {
255+
$uri .= '//' . $authority;
256+
}
257+
258+
if ($authority !== '' && isset($this->path[0]) && $this->path[0] !== '/') {
259+
$uri .= '/' . $this->path;
260+
} elseif ($authority === '' && isset($this->path[0]) && $this->path[0] === '/') {
261+
$uri .= '/' . \ltrim($this->path, '/');
262+
} else {
263+
$uri .= $this->path;
264+
}
265+
266+
if ($this->query !== '') {
267+
$uri .= '?' . $this->query;
268+
}
269+
270+
if ($this->fragment !== '') {
271+
$uri .= '#' . $this->fragment;
272+
}
273+
274+
return $uri;
275+
}
276+
277+
/**
278+
* @param string $part
279+
* @param int $component
280+
* @return string
281+
*/
282+
private function encode($part, $component)
283+
{
284+
return \preg_replace_callback(
285+
'/(?:[^a-z0-9_\-\.~!\$&\'\(\)\*\+,;=' . ($component === \PHP_URL_PATH ? ':@\/' : ($component === \PHP_URL_QUERY || $component === \PHP_URL_FRAGMENT ? ':@\/\?' : '')) . '%]++|%(?![a-f0-9]{2}))/i',
286+
function (array $match) {
287+
return \rawurlencode($match[0]);
288+
},
289+
$part
290+
);
291+
}
292+
}

tests/BrowserTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Psr\Http\Message\RequestInterface;
66
use React\Http\Browser;
77
use React\Promise\Promise;
8-
use RingCentral\Psr7\Uri;
98

109
class BrowserTest extends TestCase
1110
{

tests/Io/AbstractRequestTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
use Psr\Http\Message\StreamInterface;
66
use Psr\Http\Message\UriInterface;
77
use React\Http\Io\AbstractRequest;
8+
use React\Http\Message\Uri;
89
use React\Tests\Http\TestCase;
9-
use RingCentral\Psr7\Uri;
1010

1111
class RequestMock extends AbstractRequest
1212
{

tests/Io/ClientConnectionManagerTest.php

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

33
namespace React\Tests\Http\Io;
44

5-
use RingCentral\Psr7\Uri;
65
use React\Http\Io\ClientConnectionManager;
6+
use React\Http\Message\Uri;
77
use React\Promise\Promise;
88
use React\Promise\PromiseInterface;
99
use React\Tests\Http\TestCase;

tests/Io/ClientRequestStreamTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
namespace React\Tests\Http\Io;
44

55
use Psr\Http\Message\ResponseInterface;
6-
use RingCentral\Psr7\Uri;
76
use React\Http\Io\ClientRequestStream;
87
use React\Http\Message\Request;
8+
use React\Http\Message\Uri;
99
use React\Promise\Deferred;
1010
use React\Promise\Promise;
1111
use React\Stream\DuplexResourceStream;

0 commit comments

Comments
 (0)