6
6
7
7
/**
8
8
* 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.
9
12
*/
10
13
class Uri implements UriInterface
11
14
{
@@ -20,27 +23,32 @@ class Uri implements UriInterface
20
23
/**
21
24
* Initializes a new URI instance by parsing a URI string.
22
25
*
23
- * @param string $uri The URI to parse.
26
+ * @param string $uri The URI to parse.
24
27
*
25
28
* @throws \InvalidArgumentException If the given URI cannot be parsed.
26
29
*/
27
- public function __construct (string $ uri )
30
+ public function __construct (string $ uri = '' )
28
31
{
32
+ if ($ uri === '' ) {
33
+ return ;
34
+ }
35
+
29
36
$ parts = parse_url ($ uri );
30
37
31
- if (! $ parts ) {
38
+ if ($ parts === false ) {
32
39
throw new \InvalidArgumentException ("Invalid URI: $ uri " );
33
40
}
34
41
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 ' ] : '' ;
41
48
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 : '' );
44
52
}
45
53
}
46
54
@@ -57,11 +65,11 @@ public function getScheme(): string
57
65
*/
58
66
public function getAuthority (): string
59
67
{
60
- $ authority = $ this ->getUserInfo () !== '' ? $ this ->getUserInfo (). '@ ' : '' ;
68
+ $ authority = $ this ->getUserInfo () !== '' ? $ this ->getUserInfo () . '@ ' : '' ;
61
69
$ authority .= $ this ->host ;
62
70
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 ;
65
73
}
66
74
67
75
return $ authority ;
@@ -120,8 +128,12 @@ public function getFragment(): string
120
128
*/
121
129
public function withScheme (string $ scheme ): UriInterface
122
130
{
131
+ if ($ this ->scheme === $ scheme ) {
132
+ return $ this ;
133
+ }
134
+
123
135
$ clone = clone $ this ;
124
- $ clone ->scheme = $ scheme ;
136
+ $ clone ->scheme = strtolower ( $ scheme) ;
125
137
126
138
return $ clone ;
127
139
}
@@ -131,8 +143,14 @@ public function withScheme(string $scheme): UriInterface
131
143
*/
132
144
public function withUserInfo (string $ user , ?string $ password = null ): UriInterface
133
145
{
146
+ $ userInfo = $ user . ($ password !== null ? ': ' . $ password : '' );
147
+
148
+ if ($ this ->userInfo === $ userInfo ) {
149
+ return $ this ;
150
+ }
151
+
134
152
$ clone = clone $ this ;
135
- $ clone ->userInfo = $ user .( $ password !== null ? ' : ' . $ password : '' ) ;
153
+ $ clone ->userInfo = $ userInfo ;
136
154
137
155
return $ clone ;
138
156
}
@@ -142,8 +160,12 @@ public function withUserInfo(string $user, ?string $password = null): UriInterfa
142
160
*/
143
161
public function withHost (string $ host ): UriInterface
144
162
{
163
+ if ($ this ->host === $ host ) {
164
+ return $ this ;
165
+ }
166
+
145
167
$ clone = clone $ this ;
146
- $ clone ->host = $ host ;
168
+ $ clone ->host = strtolower ( $ host) ;
147
169
148
170
return $ clone ;
149
171
}
@@ -153,6 +175,14 @@ public function withHost(string $host): UriInterface
153
175
*/
154
176
public function withPort (?int $ port ): UriInterface
155
177
{
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
+
156
186
$ clone = clone $ this ;
157
187
$ clone ->port = $ port ;
158
188
@@ -164,6 +194,10 @@ public function withPort(?int $port): UriInterface
164
194
*/
165
195
public function withPath (string $ path ): UriInterface
166
196
{
197
+ if ($ this ->path === $ path ) {
198
+ return $ this ;
199
+ }
200
+
167
201
$ clone = clone $ this ;
168
202
$ clone ->path = $ path ;
169
203
@@ -175,6 +209,10 @@ public function withPath(string $path): UriInterface
175
209
*/
176
210
public function withQuery (string $ query ): UriInterface
177
211
{
212
+ if ($ this ->query === $ query ) {
213
+ return $ this ;
214
+ }
215
+
178
216
$ clone = clone $ this ;
179
217
$ clone ->query = $ query ;
180
218
@@ -186,6 +224,10 @@ public function withQuery(string $query): UriInterface
186
224
*/
187
225
public function withFragment (string $ fragment ): UriInterface
188
226
{
227
+ if ($ this ->fragment === $ fragment ) {
228
+ return $ this ;
229
+ }
230
+
189
231
$ clone = clone $ this ;
190
232
$ clone ->fragment = $ fragment ;
191
233
@@ -199,24 +241,45 @@ public function __toString(): string
199
241
{
200
242
$ uri = '' ;
201
243
202
- if ($ this ->scheme ) {
203
- $ uri .= $ this ->scheme . ':// ' ;
244
+ if ($ this ->scheme !== '' ) {
245
+ $ uri .= $ this ->scheme . ':// ' ;
204
246
}
205
247
206
- $ uri .= $ this ->getAuthority ();
248
+ $ authority = $ this ->getAuthority ();
249
+ if ($ authority !== '' ) {
250
+ $ uri .= $ authority ;
251
+ }
207
252
208
- if ($ this ->path ) {
253
+ if ($ this ->path !== '' ) {
209
254
$ uri .= $ this ->path ;
210
255
}
211
256
212
- if ($ this ->query ) {
213
- $ uri .= '? ' . $ this ->query ;
257
+ if ($ this ->query !== '' ) {
258
+ $ uri .= '? ' . $ this ->query ;
214
259
}
215
260
216
- if ($ this ->fragment ) {
217
- $ uri .= '# ' . $ this ->fragment ;
261
+ if ($ this ->fragment !== '' ) {
262
+ $ uri .= '# ' . $ this ->fragment ;
218
263
}
219
264
220
265
return $ uri ;
221
266
}
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