Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.11% |
111 / 112 |
|
97.14% |
34 / 35 |
CRAP | |
0.00% |
0 / 1 |
URL | |
99.11% |
111 / 112 |
|
97.14% |
34 / 35 |
60 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addQuery | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addQueries | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
filterQuery | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getBaseUrl | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getFragment | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHost | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getHostname | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOrigin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getParsedUrl | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
getPass | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPathSegments | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPathSegment | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPort | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPortPart | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getQuery | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getQueryData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getScheme | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAsString | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
toString | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
getUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeQueryData | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setFragment | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setHostname | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
setPass | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setPathSegments | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setPort | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
setQuery | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setQueryData | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setScheme | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setUrl | |
95.24% |
20 / 21 |
|
0.00% |
0 / 1 |
9 | |||
setUser | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
jsonSerialize | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework HTTP Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\HTTP; |
11 | |
12 | use InvalidArgumentException; |
13 | use JetBrains\PhpStorm\ArrayShape; |
14 | use JetBrains\PhpStorm\Deprecated; |
15 | use JetBrains\PhpStorm\Pure; |
16 | use RuntimeException; |
17 | |
18 | /** |
19 | * Class URL. |
20 | * |
21 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web#urls |
22 | * @see https://developer.mozilla.org/en-US/docs/Web/API/URL |
23 | * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3 |
24 | * |
25 | * @package http |
26 | */ |
27 | class URL implements \JsonSerializable, \Stringable |
28 | { |
29 | /** |
30 | * The #fragment (id). |
31 | */ |
32 | protected ?string $fragment = null; |
33 | protected ?string $hostname = null; |
34 | protected ?string $pass = null; |
35 | /** |
36 | * The /paths/of/url. |
37 | * |
38 | * @var array<int,string> |
39 | */ |
40 | protected array $pathSegments = []; |
41 | protected ?int $port = null; |
42 | /** |
43 | * The ?queries. |
44 | * |
45 | * @var array<string,mixed> |
46 | */ |
47 | protected array $queryData = []; |
48 | protected ?string $scheme = null; |
49 | protected ?string $user = null; |
50 | |
51 | /** |
52 | * URL constructor. |
53 | * |
54 | * @param string $url |
55 | */ |
56 | public function __construct(string $url) |
57 | { |
58 | $this->setUrl($url); |
59 | } |
60 | |
61 | /** |
62 | * @return string |
63 | */ |
64 | #[Pure] |
65 | public function __toString() : string |
66 | { |
67 | return $this->toString(); |
68 | } |
69 | |
70 | /** |
71 | * @param string $query |
72 | * @param int|string|null $value |
73 | * |
74 | * @return static |
75 | */ |
76 | public function addQuery(string $query, $value = null) : static |
77 | { |
78 | $this->queryData[$query] = $value; |
79 | return $this; |
80 | } |
81 | |
82 | /** |
83 | * @param array<string,int|string|null> $queries |
84 | * |
85 | * @return static |
86 | */ |
87 | public function addQueries(array $queries) : static |
88 | { |
89 | foreach ($queries as $name => $value) { |
90 | $this->addQuery($name, $value); |
91 | } |
92 | return $this; |
93 | } |
94 | |
95 | /** |
96 | * @param array<int,string> $allowed |
97 | * |
98 | * @return array<string,mixed> |
99 | */ |
100 | #[Pure] |
101 | protected function filterQuery(array $allowed) : array |
102 | { |
103 | return $this->queryData ? |
104 | \array_intersect_key($this->queryData, \array_flip($allowed)) |
105 | : []; |
106 | } |
107 | |
108 | #[Pure] |
109 | public function getBaseUrl(string $path = '/') : string |
110 | { |
111 | if ($path && $path !== '/') { |
112 | $path = '/' . \ltrim($path, '/'); |
113 | } |
114 | return $this->getOrigin() . $path; |
115 | } |
116 | |
117 | /** |
118 | * @return string|null |
119 | */ |
120 | public function getFragment() : ?string |
121 | { |
122 | return $this->fragment; |
123 | } |
124 | |
125 | /** |
126 | * @return string|null |
127 | */ |
128 | #[Pure] |
129 | public function getHost() : ?string |
130 | { |
131 | return $this->hostname === null ? null : $this->hostname . $this->getPortPart(); |
132 | } |
133 | |
134 | #[Pure] |
135 | public function getHostname() : ?string |
136 | { |
137 | return $this->hostname; |
138 | } |
139 | |
140 | #[Pure] |
141 | public function getOrigin() : string |
142 | { |
143 | return $this->getScheme() . '://' . $this->getHost(); |
144 | } |
145 | |
146 | /** |
147 | * @return array<string,mixed> |
148 | */ |
149 | #[ArrayShape([ |
150 | 'scheme' => 'string', |
151 | 'user' => 'null|string', |
152 | 'pass' => 'null|string', |
153 | 'hostname' => 'string', |
154 | 'port' => 'int|null', |
155 | 'path' => 'string[]', |
156 | 'query' => 'mixed[]', |
157 | 'fragment' => 'null|string', |
158 | ])] |
159 | #[Pure] |
160 | public function getParsedUrl() : array |
161 | { |
162 | return [ |
163 | 'scheme' => $this->getScheme(), |
164 | 'user' => $this->getUser(), |
165 | 'pass' => $this->getPass(), |
166 | 'hostname' => $this->getHostname(), |
167 | 'port' => $this->getPort(), |
168 | 'path' => $this->getPathSegments(), |
169 | 'query' => $this->getQueryData(), |
170 | 'fragment' => $this->getFragment(), |
171 | ]; |
172 | } |
173 | |
174 | /** |
175 | * @return string|null |
176 | */ |
177 | #[Pure] |
178 | public function getPass() : ?string |
179 | { |
180 | return $this->pass; |
181 | } |
182 | |
183 | #[Pure] |
184 | public function getPath() : string |
185 | { |
186 | return '/' . \implode('/', $this->pathSegments); |
187 | } |
188 | |
189 | /** |
190 | * @return array<int,string> |
191 | */ |
192 | #[Pure] |
193 | public function getPathSegments() : array |
194 | { |
195 | return $this->pathSegments; |
196 | } |
197 | |
198 | #[Pure] |
199 | public function getPathSegment(int $index) : ?string |
200 | { |
201 | return $this->pathSegments[$index] ?? null; |
202 | } |
203 | |
204 | /** |
205 | * @return int|null |
206 | */ |
207 | #[Pure] |
208 | public function getPort() : ?int |
209 | { |
210 | return $this->port; |
211 | } |
212 | |
213 | #[Pure] |
214 | protected function getPortPart() : string |
215 | { |
216 | $part = $this->getPort(); |
217 | if ( ! \in_array($part, [ |
218 | null, |
219 | 80, |
220 | 443, |
221 | ], true)) { |
222 | return ':' . $part; |
223 | } |
224 | return ''; |
225 | } |
226 | |
227 | /** |
228 | * Get the "Query" part of the URL. |
229 | * |
230 | * @param array<int,string> $allowedKeys Allowed query keys |
231 | * |
232 | * @return string|null |
233 | */ |
234 | #[Pure] |
235 | public function getQuery(array $allowedKeys = []) : ?string |
236 | { |
237 | $query = $this->getQueryData($allowedKeys); |
238 | return $query ? \http_build_query($query) : null; |
239 | } |
240 | |
241 | /** |
242 | * @param array<int,string> $allowedKeys |
243 | * |
244 | * @return array<string,mixed> |
245 | */ |
246 | #[Pure] |
247 | public function getQueryData(array $allowedKeys = []) : array |
248 | { |
249 | return $allowedKeys ? $this->filterQuery($allowedKeys) : $this->queryData; |
250 | } |
251 | |
252 | /** |
253 | * @return string|null |
254 | */ |
255 | #[Pure] |
256 | public function getScheme() : ?string |
257 | { |
258 | return $this->scheme; |
259 | } |
260 | |
261 | /** |
262 | * @return string |
263 | * |
264 | * @deprecated Use {@see URL::toString()} |
265 | * |
266 | * @codeCoverageIgnore |
267 | */ |
268 | #[Deprecated( |
269 | reason: 'since HTTP Library version 5.3, use toString() instead', |
270 | replacement: '%class%->toString()' |
271 | )] |
272 | public function getAsString() : string |
273 | { |
274 | \trigger_error( |
275 | 'Method ' . __METHOD__ . ' is deprecated', |
276 | \E_USER_DEPRECATED |
277 | ); |
278 | return $this->toString(); |
279 | } |
280 | |
281 | /** |
282 | * @since 5.3 |
283 | * |
284 | * @return string |
285 | */ |
286 | #[Pure] |
287 | public function toString() : string |
288 | { |
289 | $url = $this->getScheme() . '://'; |
290 | $part = $this->getUser(); |
291 | if ($part !== null) { |
292 | $url .= $part; |
293 | $part = $this->getPass(); |
294 | if ($part !== null) { |
295 | $url .= ':' . $part; |
296 | } |
297 | $url .= '@'; |
298 | } |
299 | $url .= $this->getHost(); |
300 | $url .= $this->getPath(); |
301 | $part = $this->getQuery(); |
302 | if ($part !== null) { |
303 | $url .= '?' . $part; |
304 | } |
305 | $part = $this->getFragment(); |
306 | if ($part !== null) { |
307 | $url .= '#' . $part; |
308 | } |
309 | return $url; |
310 | } |
311 | |
312 | /** |
313 | * @return string|null |
314 | */ |
315 | #[Pure] |
316 | public function getUser() : ?string |
317 | { |
318 | return $this->user; |
319 | } |
320 | |
321 | /** |
322 | * @param string $key |
323 | * |
324 | * @return static |
325 | */ |
326 | public function removeQueryData(string $key) : static |
327 | { |
328 | unset($this->queryData[$key]); |
329 | return $this; |
330 | } |
331 | |
332 | /** |
333 | * @param string $fragment |
334 | * |
335 | * @return static |
336 | */ |
337 | public function setFragment(string $fragment) : static |
338 | { |
339 | $this->fragment = \ltrim($fragment, '#'); |
340 | return $this; |
341 | } |
342 | |
343 | /** |
344 | * @param string $hostname |
345 | * |
346 | * @throws InvalidArgumentException for invalid URL Hostname |
347 | * |
348 | * @return static |
349 | */ |
350 | public function setHostname(string $hostname) : static |
351 | { |
352 | $filtered = \filter_var($hostname, \FILTER_VALIDATE_DOMAIN, \FILTER_FLAG_HOSTNAME); |
353 | if ( ! $filtered) { |
354 | throw new InvalidArgumentException("Invalid URL Hostname: {$hostname}"); |
355 | } |
356 | $this->hostname = $filtered; |
357 | return $this; |
358 | } |
359 | |
360 | /** |
361 | * @param string $pass |
362 | * |
363 | * @return static |
364 | */ |
365 | public function setPass(string $pass) : static |
366 | { |
367 | $this->pass = $pass; |
368 | return $this; |
369 | } |
370 | |
371 | /** |
372 | * @param string $segments |
373 | * |
374 | * @return static |
375 | */ |
376 | public function setPath(string $segments) : static |
377 | { |
378 | return $this->setPathSegments(\explode('/', \ltrim($segments, '/'))); |
379 | } |
380 | |
381 | /** |
382 | * @param array<int,string> $segments |
383 | * |
384 | * @return static |
385 | */ |
386 | public function setPathSegments(array $segments) : static |
387 | { |
388 | $this->pathSegments = $segments; |
389 | return $this; |
390 | } |
391 | |
392 | /** |
393 | * @param int $port |
394 | * |
395 | * @throws InvalidArgumentException for invalid URL Port |
396 | * |
397 | * @return static |
398 | */ |
399 | public function setPort(int $port) : static |
400 | { |
401 | if ($port < 1 || $port > 65535) { |
402 | throw new InvalidArgumentException("Invalid URL Port: {$port}"); |
403 | } |
404 | $this->port = $port; |
405 | return $this; |
406 | } |
407 | |
408 | /** |
409 | * @param string $data |
410 | * @param array<string> $only |
411 | * |
412 | * @return static |
413 | */ |
414 | public function setQuery(string $data, array $only = []) : static |
415 | { |
416 | \parse_str(\ltrim($data, '?'), $data); |
417 | return $this->setQueryData($data, $only); |
418 | } |
419 | |
420 | /** |
421 | * @param array<mixed> $data |
422 | * @param array<string> $only |
423 | * |
424 | * @return static |
425 | */ |
426 | public function setQueryData(array $data, array $only = []) : static |
427 | { |
428 | if ($only) { |
429 | $data = \array_intersect_key($data, \array_flip($only)); |
430 | } |
431 | $this->queryData = $data; |
432 | return $this; |
433 | } |
434 | |
435 | /** |
436 | * @param string $scheme |
437 | * |
438 | * @return static |
439 | */ |
440 | public function setScheme(string $scheme) : static |
441 | { |
442 | $this->scheme = $scheme; |
443 | return $this; |
444 | } |
445 | |
446 | /** |
447 | * @param string $url |
448 | * |
449 | * @throws InvalidArgumentException for invalid URL |
450 | * |
451 | * @return static |
452 | */ |
453 | protected function setUrl(string $url) : static |
454 | { |
455 | $filteredUrl = \filter_var($url, \FILTER_VALIDATE_URL); |
456 | if ( ! $filteredUrl) { |
457 | throw new InvalidArgumentException("Invalid URL: {$url}"); |
458 | } |
459 | $url = \parse_url($filteredUrl); |
460 | if ($url === false) { |
461 | throw new RuntimeException("URL could not be parsed: {$filteredUrl}"); |
462 | } |
463 | $this->setScheme($url['scheme']); // @phpstan-ignore-line |
464 | if (isset($url['user'])) { |
465 | $this->setUser($url['user']); |
466 | } |
467 | if (isset($url['pass'])) { |
468 | $this->setPass($url['pass']); |
469 | } |
470 | $this->setHostname($url['host']); // @phpstan-ignore-line |
471 | if (isset($url['port'])) { |
472 | $this->setPort($url['port']); |
473 | } |
474 | if (isset($url['path'])) { |
475 | $this->setPath($url['path']); |
476 | } |
477 | if (isset($url['query'])) { |
478 | $this->setQuery($url['query']); |
479 | } |
480 | if (isset($url['fragment'])) { |
481 | $this->setFragment($url['fragment']); |
482 | } |
483 | return $this; |
484 | } |
485 | |
486 | /** |
487 | * @param string $user |
488 | * |
489 | * @return static |
490 | */ |
491 | public function setUser(string $user) : static |
492 | { |
493 | $this->user = $user; |
494 | return $this; |
495 | } |
496 | |
497 | #[Pure] |
498 | public function jsonSerialize() : string |
499 | { |
500 | return $this->toString(); |
501 | } |
502 | } |