Skip to content

Commit 042b4eb

Browse files
committed
Improve URI class type safety and immutability optimizations
- Add strict type checking for parse_url() results with explicit type validation - Implement early returns in withX() methods when values are unchanged - Add port range validation (1-65535) in withPort() method - Normalize scheme and host to lowercase for consistency - Add isStandardPort() helper method supporting HTTP, HTTPS, FTP, and FTPS - Allow empty URI string in constructor with early return optimization - Enhance class documentation with detailed component description
1 parent 4839870 commit 042b4eb

File tree

1 file changed

+89
-26
lines changed

1 file changed

+89
-26
lines changed

src/Http/Uri.php

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
/**
88
* A PSR-7 compliant implementation of a Uniform Resource Identifier (URI).
9+
*
10+
* Provides parsing and manipulation of URI components including scheme, host, port,
11+
* path, query, fragment, and user information with immutable operations.
912
*/
1013
class Uri implements UriInterface
1114
{
@@ -20,27 +23,32 @@ class Uri implements UriInterface
2023
/**
2124
* Initializes a new URI instance by parsing a URI string.
2225
*
23-
* @param string $uri The URI to parse.
26+
* @param string $uri The URI to parse.
2427
*
2528
* @throws \InvalidArgumentException If the given URI cannot be parsed.
2629
*/
27-
public function __construct(string $uri)
30+
public function __construct(string $uri = '')
2831
{
32+
if ($uri === '') {
33+
return;
34+
}
35+
2936
$parts = parse_url($uri);
3037

31-
if (! $parts) {
38+
if ($parts === false) {
3239
throw new \InvalidArgumentException("Invalid URI: $uri");
3340
}
3441

35-
$this->scheme = $parts['scheme'] ?? '';
36-
$this->host = $parts['host'] ?? '';
37-
$this->port = $parts['port'] ?? null;
38-
$this->path = $parts['path'] ?? '/';
39-
$this->query = $parts['query'] ?? '';
40-
$this->fragment = $parts['fragment'] ?? '';
42+
$this->scheme = isset($parts['scheme']) && is_string($parts['scheme']) ? $parts['scheme'] : '';
43+
$this->host = isset($parts['host']) && is_string($parts['host']) ? $parts['host'] : '';
44+
$this->port = isset($parts['port']) && is_int($parts['port']) ? $parts['port'] : null;
45+
$this->path = isset($parts['path']) && is_string($parts['path']) ? $parts['path'] : '/';
46+
$this->query = isset($parts['query']) && is_string($parts['query']) ? $parts['query'] : '';
47+
$this->fragment = isset($parts['fragment']) && is_string($parts['fragment']) ? $parts['fragment'] : '';
4148

42-
if (isset($parts['user'])) {
43-
$this->userInfo = $parts['user'].(isset($parts['pass']) ? ':'.$parts['pass'] : '');
49+
if (isset($parts['user']) && is_string($parts['user'])) {
50+
$password = isset($parts['pass']) && is_string($parts['pass']) ? $parts['pass'] : null;
51+
$this->userInfo = $parts['user'] . ($password !== null ? ':' . $password : '');
4452
}
4553
}
4654

@@ -57,11 +65,11 @@ public function getScheme(): string
5765
*/
5866
public function getAuthority(): string
5967
{
60-
$authority = $this->getUserInfo() !== '' ? $this->getUserInfo().'@' : '';
68+
$authority = $this->getUserInfo() !== '' ? $this->getUserInfo() . '@' : '';
6169
$authority .= $this->host;
6270

63-
if ($this->port !== null && ! in_array($this->port, [80, 443])) {
64-
$authority .= ':'.$this->port;
71+
if ($this->port !== null && !$this->isStandardPort($this->scheme, $this->port)) {
72+
$authority .= ':' . $this->port;
6573
}
6674

6775
return $authority;
@@ -120,8 +128,12 @@ public function getFragment(): string
120128
*/
121129
public function withScheme(string $scheme): UriInterface
122130
{
131+
if ($this->scheme === $scheme) {
132+
return $this;
133+
}
134+
123135
$clone = clone $this;
124-
$clone->scheme = $scheme;
136+
$clone->scheme = strtolower($scheme);
125137

126138
return $clone;
127139
}
@@ -131,8 +143,14 @@ public function withScheme(string $scheme): UriInterface
131143
*/
132144
public function withUserInfo(string $user, ?string $password = null): UriInterface
133145
{
146+
$userInfo = $user . ($password !== null ? ':' . $password : '');
147+
148+
if ($this->userInfo === $userInfo) {
149+
return $this;
150+
}
151+
134152
$clone = clone $this;
135-
$clone->userInfo = $user.($password !== null ? ':'.$password : '');
153+
$clone->userInfo = $userInfo;
136154

137155
return $clone;
138156
}
@@ -142,8 +160,12 @@ public function withUserInfo(string $user, ?string $password = null): UriInterfa
142160
*/
143161
public function withHost(string $host): UriInterface
144162
{
163+
if ($this->host === $host) {
164+
return $this;
165+
}
166+
145167
$clone = clone $this;
146-
$clone->host = $host;
168+
$clone->host = strtolower($host);
147169

148170
return $clone;
149171
}
@@ -153,6 +175,14 @@ public function withHost(string $host): UriInterface
153175
*/
154176
public function withPort(?int $port): UriInterface
155177
{
178+
if ($this->port === $port) {
179+
return $this;
180+
}
181+
182+
if ($port !== null && ($port < 1 || $port > 65535)) {
183+
throw new \InvalidArgumentException('Port must be between 1 and 65535 or null');
184+
}
185+
156186
$clone = clone $this;
157187
$clone->port = $port;
158188

@@ -164,6 +194,10 @@ public function withPort(?int $port): UriInterface
164194
*/
165195
public function withPath(string $path): UriInterface
166196
{
197+
if ($this->path === $path) {
198+
return $this;
199+
}
200+
167201
$clone = clone $this;
168202
$clone->path = $path;
169203

@@ -175,6 +209,10 @@ public function withPath(string $path): UriInterface
175209
*/
176210
public function withQuery(string $query): UriInterface
177211
{
212+
if ($this->query === $query) {
213+
return $this;
214+
}
215+
178216
$clone = clone $this;
179217
$clone->query = $query;
180218

@@ -186,6 +224,10 @@ public function withQuery(string $query): UriInterface
186224
*/
187225
public function withFragment(string $fragment): UriInterface
188226
{
227+
if ($this->fragment === $fragment) {
228+
return $this;
229+
}
230+
189231
$clone = clone $this;
190232
$clone->fragment = $fragment;
191233

@@ -199,24 +241,45 @@ public function __toString(): string
199241
{
200242
$uri = '';
201243

202-
if ($this->scheme) {
203-
$uri .= $this->scheme.'://';
244+
if ($this->scheme !== '') {
245+
$uri .= $this->scheme . '://';
204246
}
205247

206-
$uri .= $this->getAuthority();
248+
$authority = $this->getAuthority();
249+
if ($authority !== '') {
250+
$uri .= $authority;
251+
}
207252

208-
if ($this->path) {
253+
if ($this->path !== '') {
209254
$uri .= $this->path;
210255
}
211256

212-
if ($this->query) {
213-
$uri .= '?'.$this->query;
257+
if ($this->query !== '') {
258+
$uri .= '?' . $this->query;
214259
}
215260

216-
if ($this->fragment) {
217-
$uri .= '#'.$this->fragment;
261+
if ($this->fragment !== '') {
262+
$uri .= '#' . $this->fragment;
218263
}
219264

220265
return $uri;
221266
}
222-
}
267+
268+
/**
269+
* Checks if the given port is a standard port for the given scheme.
270+
*
271+
* @param string $scheme The URI scheme
272+
* @param int $port The port number
273+
* @return bool True if it's a standard port, false otherwise
274+
*/
275+
private function isStandardPort(string $scheme, int $port): bool
276+
{
277+
return match (strtolower($scheme)) {
278+
'http' => $port === 80,
279+
'https' => $port === 443,
280+
'ftp' => $port === 21,
281+
'ftps' => $port === 990,
282+
default => false,
283+
};
284+
}
285+
}

0 commit comments

Comments
 (0)