Skip to content

Commit ad009e9

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

File tree

8 files changed

+888
-5
lines changed

8 files changed

+888
-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: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
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+
if (\PHP_VERSION_ID < 50407 && \strpos($uri, '//') === 0) {
49+
// @link https://3v4l.org/UrAQP
50+
$parts = \parse_url('http:' . $uri);
51+
unset($parts['schema']);
52+
} else {
53+
$parts = \parse_url($uri);
54+
}
55+
56+
if ($parts === false || (isset($parts['scheme']) && !\preg_match('#^[a-z]+$#i', $parts['scheme'])) || (isset($parts['host']) && \preg_match('#[\s_%+]#', $parts['host']))) {
57+
throw new \InvalidArgumentException('Invalid URI given');
58+
}
59+
60+
if (isset($parts['scheme'])) {
61+
$this->scheme = \strtolower($parts['scheme']);
62+
}
63+
64+
if (isset($parts['user']) || isset($parts['pass'])) {
65+
$this->userInfo = $this->encode(isset($parts['user']) ? $parts['user'] : '', \PHP_URL_USER) . (isset($parts['pass']) ? ':' . $this->encode($parts['pass'], \PHP_URL_PASS) : '');
66+
}
67+
68+
if (isset($parts['host'])) {
69+
$this->host = \strtolower($parts['host']);
70+
}
71+
72+
if (isset($parts['port']) && !(($parts['port'] === 80 && $this->scheme === 'http') || ($parts['port'] === 443 && $this->scheme === 'https'))) {
73+
$this->port = $parts['port'];
74+
}
75+
76+
if (isset($parts['path'])) {
77+
$this->path = $this->encode($parts['path'], \PHP_URL_PATH);
78+
}
79+
80+
if (isset($parts['query'])) {
81+
$this->query = $this->encode($parts['query'], \PHP_URL_QUERY);
82+
}
83+
84+
if (isset($parts['fragment'])) {
85+
$this->fragment = $this->encode($parts['fragment'], \PHP_URL_FRAGMENT);
86+
}
87+
}
88+
89+
public function getScheme()
90+
{
91+
return $this->scheme;
92+
}
93+
94+
public function getAuthority()
95+
{
96+
if ($this->host === '') {
97+
return '';
98+
}
99+
100+
return ($this->userInfo !== '' ? $this->userInfo . '@' : '') . $this->host . ($this->port !== null ? ':' . $this->port : '');
101+
}
102+
103+
public function getUserInfo()
104+
{
105+
return $this->userInfo;
106+
}
107+
108+
public function getHost()
109+
{
110+
return $this->host;
111+
}
112+
113+
public function getPort()
114+
{
115+
return $this->port;
116+
}
117+
118+
public function getPath()
119+
{
120+
return $this->path;
121+
}
122+
123+
public function getQuery()
124+
{
125+
return $this->query;
126+
}
127+
128+
public function getFragment()
129+
{
130+
return $this->fragment;
131+
}
132+
133+
public function withScheme($scheme)
134+
{
135+
$scheme = \strtolower($scheme);
136+
if ($scheme === $this->scheme) {
137+
return $this;
138+
}
139+
140+
if (!\preg_match('#^[a-z]*$#', $scheme)) {
141+
throw new \InvalidArgumentException('Invalid URI scheme given');
142+
}
143+
144+
$new = clone $this;
145+
$new->scheme = $scheme;
146+
147+
if (($this->port === 80 && $scheme === 'http') || ($this->port === 443 && $scheme === 'https')) {
148+
$new->port = null;
149+
}
150+
151+
return $new;
152+
}
153+
154+
public function withUserInfo($user, $password = null)
155+
{
156+
$userInfo = $this->encode($user, \PHP_URL_USER) . ($password !== null ? ':' . $this->encode($password, \PHP_URL_PASS) : '');
157+
if ($userInfo === $this->userInfo) {
158+
return $this;
159+
}
160+
161+
$new = clone $this;
162+
$new->userInfo = $userInfo;
163+
164+
return $new;
165+
}
166+
167+
public function withHost($host)
168+
{
169+
$host = \strtolower($host);
170+
if ($host === $this->host) {
171+
return $this;
172+
}
173+
174+
if (\preg_match('#[\s_%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) {
175+
throw new \InvalidArgumentException('Invalid URI host given');
176+
}
177+
178+
$new = clone $this;
179+
$new->host = $host;
180+
181+
return $new;
182+
}
183+
184+
public function withPort($port)
185+
{
186+
$port = $port === null ? null : (int) $port;
187+
if (($port === 80 && $this->scheme === 'http') || ($port === 443 && $this->scheme === 'https')) {
188+
$port = null;
189+
}
190+
191+
if ($port === $this->port) {
192+
return $this;
193+
}
194+
195+
if ($port !== null && ($port < 1 || $port > 0xffff)) {
196+
throw new \InvalidArgumentException('Invalid URI port given');
197+
}
198+
199+
$new = clone $this;
200+
$new->port = $port;
201+
202+
return $new;
203+
}
204+
205+
public function withPath($path)
206+
{
207+
$path = $this->encode($path, \PHP_URL_PATH);
208+
if ($path === $this->path) {
209+
return $this;
210+
}
211+
212+
$new = clone $this;
213+
$new->path = $path;
214+
215+
return $new;
216+
}
217+
218+
public function withQuery($query)
219+
{
220+
$query = $this->encode($query, \PHP_URL_QUERY);
221+
if ($query === $this->query) {
222+
return $this;
223+
}
224+
225+
$new = clone $this;
226+
$new->query = $query;
227+
228+
return $new;
229+
}
230+
231+
public function withFragment($fragment)
232+
{
233+
$fragment = $this->encode($fragment, \PHP_URL_FRAGMENT);
234+
if ($fragment === $this->fragment) {
235+
return $this;
236+
}
237+
238+
$new = clone $this;
239+
$new->fragment = $fragment;
240+
241+
return $new;
242+
}
243+
244+
public function __toString()
245+
{
246+
$uri = '';
247+
if ($this->scheme !== '') {
248+
$uri .= $this->scheme . ':';
249+
}
250+
251+
$authority = $this->getAuthority();
252+
if ($authority !== '') {
253+
$uri .= '//' . $authority;
254+
}
255+
256+
if ($authority !== '' && isset($this->path[0]) && $this->path[0] !== '/') {
257+
$uri .= '/' . $this->path;
258+
} elseif ($authority === '' && isset($this->path[0]) && $this->path[0] === '/') {
259+
$uri .= '/' . \ltrim($this->path, '/');
260+
} else {
261+
$uri .= $this->path;
262+
}
263+
264+
if ($this->query !== '') {
265+
$uri .= '?' . $this->query;
266+
}
267+
268+
if ($this->fragment !== '') {
269+
$uri .= '#' . $this->fragment;
270+
}
271+
272+
return $uri;
273+
}
274+
275+
/**
276+
* @param string $part
277+
* @param int $component
278+
* @return string
279+
*/
280+
private function encode($part, $component)
281+
{
282+
return \preg_replace_callback(
283+
'/(?:[^a-z0-9_\-\.~!\$&\'\(\)\*\+,;=' . ($component === \PHP_URL_PATH ? ':@\/' : ($component === \PHP_URL_QUERY || $component === \PHP_URL_FRAGMENT ? ':@\/\?' : '')) . '%]++|%(?![a-f0-9]{2}))/i',
284+
function (array $match) {
285+
return \rawurlencode($match[0]);
286+
},
287+
$part
288+
);
289+
}
290+
}

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)