1 | <?php |
||
2 | |||
3 | namespace HughCube\PUrl; |
||
4 | |||
5 | use HughCube\PUrl\Exceptions\InvalidArgumentException; |
||
6 | use Psr\Http\Message\UriInterface; |
||
7 | |||
8 | class Url implements UriInterface |
||
9 | { |
||
10 | /** |
||
11 | * @var int[] |
||
12 | */ |
||
13 | private $schemes = [ |
||
14 | 'http' => 80, |
||
15 | 'https' => 443, |
||
16 | ]; |
||
17 | /** |
||
18 | * @var string|null url scheme |
||
19 | */ |
||
20 | private $scheme; |
||
21 | /** |
||
22 | * @var string|null url host |
||
23 | */ |
||
24 | private $host; |
||
25 | /** |
||
26 | * @var int|null url port |
||
27 | */ |
||
28 | private $port; |
||
29 | /** |
||
30 | * @var string|null url user |
||
31 | */ |
||
32 | private $user; |
||
33 | /** |
||
34 | * @var string|null url pass |
||
35 | */ |
||
36 | private $pass; |
||
37 | /** |
||
38 | * @var string|null url path |
||
39 | */ |
||
40 | private $path; |
||
41 | /** |
||
42 | * @var string|null url query string |
||
43 | */ |
||
44 | private $query; |
||
45 | /** |
||
46 | * @var string|null url fragment |
||
47 | */ |
||
48 | private $fragment; |
||
49 | |||
50 | /** |
||
51 | * 获取实例. |
||
52 | * |
||
53 | * @param mixed $url |
||
54 | * |
||
55 | * @return static |
||
56 | */ |
||
57 | 13 | public static function instance($url = null) |
|
58 | { |
||
59 | 13 | return new static($url); |
|
60 | } |
||
61 | |||
62 | /** |
||
63 | * @param mixed $url |
||
64 | * |
||
65 | * @return Url|null |
||
66 | */ |
||
67 | 7 | public static function parse($url) |
|
68 | { |
||
69 | try { |
||
70 | 7 | $url = static::instance($url); |
|
71 | 6 | if ($url instanceof self && static::isUrlString($url->toString())) { |
|
72 | 6 | return $url; |
|
73 | } |
||
74 | 1 | } catch (\Throwable $exception) { |
|
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
![]() |
|||
75 | } |
||
76 | |||
77 | 1 | return null; |
|
78 | } |
||
79 | |||
80 | /** |
||
81 | * Url constructor. |
||
82 | * |
||
83 | * @param null|string|string[]|UriInterface $url |
||
84 | */ |
||
85 | 13 | final protected function __construct($url = null) |
|
86 | { |
||
87 | 13 | if ($url instanceof UriInterface) { |
|
88 | 8 | $this->parsePsrUrl($url); |
|
89 | 13 | } elseif (is_string($url)) { |
|
90 | 10 | $this->parseStringUrl($url); |
|
91 | 5 | } elseif (is_array($url)) { |
|
92 | 4 | $this->parseArrayUrl($url); |
|
93 | } |
||
94 | 12 | } |
|
95 | |||
96 | /** |
||
97 | * 解析 Psr 标准库的url. |
||
98 | * |
||
99 | * @param UriInterface $url |
||
100 | * |
||
101 | * @return $this |
||
102 | */ |
||
103 | 8 | private function parsePsrUrl(UriInterface $url) |
|
104 | { |
||
105 | 8 | $this->scheme = empty($scheme = $url->getScheme()) ? null : $scheme; |
|
106 | 8 | $this->host = empty($host = $url->getHost()) ? null : $host; |
|
107 | 8 | $this->port = empty($port = $url->getPort()) ? null : $port; |
|
108 | 8 | $this->path = empty($path = $url->getPath()) ? null : $path; |
|
109 | 8 | $this->query = empty($query = $url->getQuery()) ? null : $query; |
|
110 | 8 | $this->fragment = empty($fragment = $url->getFragment()) ? null : $fragment; |
|
111 | 8 | $user = $this->getUserInfo(); |
|
112 | 8 | $user = explode(':', $user); |
|
113 | 8 | $this->user = (is_array($user) && isset($user[0])) ? $user[0] : null; |
|
114 | 8 | $this->pass = (is_array($user) && isset($user[1])) ? $user[1] : null; |
|
115 | |||
116 | 8 | return $this; |
|
117 | } |
||
118 | |||
119 | /** |
||
120 | * 解析字符串url. |
||
121 | * |
||
122 | * @param string $url |
||
123 | * |
||
124 | * @return $this |
||
125 | */ |
||
126 | 10 | private function parseStringUrl($url) |
|
127 | { |
||
128 | 10 | if (!static::isUrlString($url)) { |
|
129 | 1 | throw new InvalidArgumentException('the parameter must be a url'); |
|
130 | } |
||
131 | /** @var string[] $parts */ |
||
132 | 9 | $parts = parse_url($url); |
|
133 | 9 | $this->parseArrayUrl($parts); |
|
134 | |||
135 | 9 | return $this; |
|
136 | } |
||
137 | |||
138 | /** |
||
139 | * 解析数组url. |
||
140 | * |
||
141 | * @param string[]|int[] $parts |
||
142 | * |
||
143 | * @return $this |
||
144 | */ |
||
145 | 11 | private function parseArrayUrl(array $parts) |
|
146 | { |
||
147 | 11 | $this->scheme = isset($parts['scheme']) ? $parts['scheme'] : null; |
|
148 | 11 | $this->host = isset($parts['host']) ? $parts['host'] : null; |
|
149 | 11 | $this->port = isset($parts['port']) ? $parts['port'] : null; |
|
150 | 11 | $this->user = isset($parts['user']) ? $parts['user'] : null; |
|
151 | 11 | $this->pass = isset($parts['pass']) ? $parts['pass'] : null; |
|
152 | 11 | $this->path = isset($parts['path']) ? $parts['path'] : null; |
|
153 | 11 | $this->query = isset($parts['query']) ? $parts['query'] : null; |
|
154 | 11 | $this->fragment = isset($parts['fragment']) ? $parts['fragment'] : null; |
|
155 | |||
156 | 11 | return $this; |
|
157 | } |
||
158 | |||
159 | /** |
||
160 | * 填充 Psr 标准库的url. |
||
161 | * |
||
162 | * @param UriInterface $url |
||
163 | * |
||
164 | * @return UriInterface |
||
165 | */ |
||
166 | public function fillPsrUri(UriInterface $url) |
||
167 | { |
||
168 | return $url->withScheme($this->getScheme()) |
||
169 | ->withUserInfo($this->getUser(), $this->getPass()) |
||
170 | ->withHost($this->getHost()) |
||
171 | ->withPort($this->getPort()) |
||
172 | ->withPath($this->getPath()) |
||
173 | ->withQuery($this->getQuery()) |
||
174 | ->withFragment($this->getFragment()); |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * {@inheritdoc} |
||
179 | */ |
||
180 | 10 | public function getScheme() |
|
181 | { |
||
182 | 10 | return strval($this->scheme); |
|
183 | } |
||
184 | |||
185 | /** |
||
186 | * {@inheritdoc} |
||
187 | */ |
||
188 | 8 | public function getAuthority() |
|
189 | { |
||
190 | 8 | $authority = $host = $this->getHost(); |
|
191 | 8 | if (empty($host)) { |
|
192 | return $authority; |
||
193 | } |
||
194 | 8 | $userInfo = $this->getUserInfo(); |
|
195 | 8 | if (!empty($userInfo)) { |
|
196 | $authority = "{$userInfo}@{$authority}"; |
||
197 | } |
||
198 | 8 | $port = $this->getPort(); |
|
199 | 8 | if ($this->isNonStandardPort() && !empty($port)) { |
|
200 | $authority = "{$authority}:{$port}"; |
||
201 | } |
||
202 | |||
203 | 8 | return $authority; |
|
204 | } |
||
205 | |||
206 | /** |
||
207 | * {@inheritdoc} |
||
208 | */ |
||
209 | 8 | public function getUserInfo() |
|
210 | { |
||
211 | 8 | $userInfo = $user = $this->getUser(); |
|
212 | 8 | if (empty($user)) { |
|
213 | 8 | return $userInfo; |
|
214 | } |
||
215 | $pass = $this->getPass(); |
||
216 | if (!empty($pass)) { |
||
217 | $userInfo = "{$userInfo}:{$pass}"; |
||
218 | } |
||
219 | |||
220 | return $userInfo; |
||
221 | } |
||
222 | |||
223 | /** |
||
224 | * 获取 url user. |
||
225 | * |
||
226 | * @return string |
||
227 | */ |
||
228 | 8 | public function getUser() |
|
229 | { |
||
230 | 8 | return strval($this->user); |
|
231 | } |
||
232 | |||
233 | /** |
||
234 | * 获取 url pass. |
||
235 | * |
||
236 | * @return string |
||
237 | */ |
||
238 | 2 | public function getPass() |
|
239 | { |
||
240 | 2 | return strval($this->pass); |
|
241 | } |
||
242 | |||
243 | /** |
||
244 | * {@inheritdoc} |
||
245 | */ |
||
246 | 9 | public function getHost() |
|
247 | { |
||
248 | 9 | return strval($this->host); |
|
249 | } |
||
250 | |||
251 | /** |
||
252 | * {@inheritdoc} |
||
253 | */ |
||
254 | 8 | public function getPort() |
|
255 | { |
||
256 | 8 | if (!empty($this->port)) { |
|
257 | 8 | return $this->port; |
|
258 | } |
||
259 | 8 | $scheme = $this->getScheme(); |
|
260 | |||
261 | 8 | return isset($this->schemes[$scheme]) ? $this->schemes[$scheme] : null; |
|
262 | } |
||
263 | |||
264 | /** |
||
265 | * {@inheritdoc} |
||
266 | */ |
||
267 | 8 | public function getPath() |
|
268 | { |
||
269 | 8 | if (empty($this->path)) { |
|
270 | return ''; |
||
271 | } |
||
272 | |||
273 | 8 | return '/' === substr($this->path, 0, 1) ? $this->path : "/{$this->path}"; |
|
274 | } |
||
275 | |||
276 | /** |
||
277 | * {@inheritdoc} |
||
278 | */ |
||
279 | 8 | public function getQuery() |
|
280 | { |
||
281 | 8 | return strval($this->query); |
|
282 | } |
||
283 | |||
284 | /** |
||
285 | * 获取query数组. |
||
286 | * |
||
287 | * @return array |
||
288 | */ |
||
289 | 2 | public function getQueryArray() |
|
290 | { |
||
291 | 2 | $query = $this->getQuery(); |
|
292 | 2 | $queryArray = []; |
|
293 | 2 | if (!empty($query)) { |
|
294 | 2 | parse_str($query, $queryArray); |
|
295 | } |
||
296 | |||
297 | 2 | return is_array($queryArray) ? $queryArray : []; |
|
298 | } |
||
299 | |||
300 | /** |
||
301 | * 是否存在query的key. |
||
302 | * |
||
303 | * @param string $key |
||
304 | * |
||
305 | * @return bool |
||
306 | */ |
||
307 | public function hasQueryKey($key) |
||
308 | { |
||
309 | $queryArray = $this->getQueryArray(); |
||
310 | |||
311 | return array_key_exists($key, $queryArray); |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * 是否存在query的key. |
||
316 | * |
||
317 | * @param string $key |
||
318 | * @param mixed $default |
||
319 | * |
||
320 | * @return array|string |
||
321 | */ |
||
322 | public function getQueryValue($key, $default = null) |
||
323 | { |
||
324 | $queryArray = $this->getQueryArray(); |
||
325 | |||
326 | return array_key_exists($key, $queryArray) ? $queryArray[$key] : $default; |
||
327 | } |
||
328 | |||
329 | /** |
||
330 | * {@inheritdoc} |
||
331 | */ |
||
332 | 8 | public function getFragment() |
|
333 | { |
||
334 | 8 | return strval($this->fragment); |
|
335 | } |
||
336 | |||
337 | /** |
||
338 | * Return the string representation as a URI reference. |
||
339 | * |
||
340 | * @return string |
||
341 | */ |
||
342 | 8 | public function toString() |
|
343 | { |
||
344 | 8 | $url = ''; |
|
345 | 8 | $scheme = $this->getScheme(); |
|
346 | 8 | if (!empty($scheme)) { |
|
347 | 8 | $url = "{$scheme}://{$url}"; |
|
348 | } |
||
349 | 8 | $authority = $this->getAuthority(); |
|
350 | 8 | if (!empty($authority)) { |
|
351 | 8 | $url = "{$url}{$authority}"; |
|
352 | } |
||
353 | 8 | $path = $this->getPath(); |
|
354 | 8 | if (!empty($path)) { |
|
355 | 8 | $url = "{$url}{$path}"; |
|
356 | } |
||
357 | 8 | $query = $this->getQuery(); |
|
358 | 8 | if (!empty($query)) { |
|
359 | 8 | $url = "{$url}?{$query}"; |
|
360 | } |
||
361 | 8 | $fragment = $this->getFragment(); |
|
362 | 8 | if (!empty($fragment)) { |
|
363 | 4 | $url = "{$url}#{$fragment}"; |
|
364 | } |
||
365 | |||
366 | 8 | return $url; |
|
367 | } |
||
368 | |||
369 | /** |
||
370 | * {@inheritdoc} |
||
371 | */ |
||
372 | public function withScheme($scheme) |
||
373 | { |
||
374 | $new = clone $this; |
||
375 | $new->scheme = $scheme; |
||
376 | |||
377 | return $new; |
||
378 | } |
||
379 | |||
380 | /** |
||
381 | * {@inheritdoc} |
||
382 | */ |
||
383 | public function withUserInfo($user, $password = null) |
||
384 | { |
||
385 | $new = clone $this; |
||
386 | $new->user = $user; |
||
387 | $new->pass = $password; |
||
388 | |||
389 | return $new; |
||
390 | } |
||
391 | |||
392 | /** |
||
393 | * {@inheritdoc} |
||
394 | */ |
||
395 | public function withHost($host) |
||
396 | { |
||
397 | $new = clone $this; |
||
398 | $new->host = $host; |
||
399 | |||
400 | return $new; |
||
401 | } |
||
402 | |||
403 | /** |
||
404 | * {@inheritdoc} |
||
405 | */ |
||
406 | public function withPort($port) |
||
407 | { |
||
408 | $new = clone $this; |
||
409 | $new->port = $port; |
||
410 | |||
411 | return $new; |
||
412 | } |
||
413 | |||
414 | /** |
||
415 | * {@inheritdoc} |
||
416 | */ |
||
417 | public function withPath($path) |
||
418 | { |
||
419 | $new = clone $this; |
||
420 | $new->path = $path; |
||
421 | |||
422 | return $new; |
||
423 | } |
||
424 | |||
425 | /** |
||
426 | * {@inheritdoc} |
||
427 | */ |
||
428 | public function withQuery($query) |
||
429 | { |
||
430 | $new = clone $this; |
||
431 | $new->query = $query; |
||
432 | |||
433 | return $new; |
||
434 | } |
||
435 | |||
436 | /** |
||
437 | * Return an instance with the specified query array. |
||
438 | * |
||
439 | * @param array $queryArray |
||
440 | * |
||
441 | * @return static |
||
442 | */ |
||
443 | public function withQueryArray(array $queryArray) |
||
444 | { |
||
445 | return $this->withQuery(http_build_query($queryArray)); |
||
446 | } |
||
447 | |||
448 | /** |
||
449 | * Create a new URI with a specific query string value removed. |
||
450 | * |
||
451 | * @param string|int $key |
||
452 | * |
||
453 | * @return static |
||
454 | */ |
||
455 | public function withoutQueryValue($key) |
||
456 | { |
||
457 | $queryArray = $this->getQueryArray(); |
||
458 | if (isset($queryArray[$key])) { |
||
459 | unset($queryArray[$key]); |
||
460 | } |
||
461 | |||
462 | return $this->withQueryArray($queryArray); |
||
463 | } |
||
464 | |||
465 | /** |
||
466 | * Create a new URI with a specific query string value. |
||
467 | * |
||
468 | * @param string $key |
||
469 | * @param string|int $value |
||
470 | * |
||
471 | * @return static |
||
472 | */ |
||
473 | public function withQueryValue($key, $value) |
||
474 | { |
||
475 | $queryArray = $this->getQueryArray(); |
||
476 | $queryArray[$key] = $value; |
||
477 | |||
478 | return $this->withQueryArray($queryArray); |
||
479 | } |
||
480 | |||
481 | /** |
||
482 | * {@inheritdoc} |
||
483 | */ |
||
484 | public function withFragment($fragment) |
||
485 | { |
||
486 | $new = clone $this; |
||
487 | $new->fragment = $fragment; |
||
488 | |||
489 | return $new; |
||
490 | } |
||
491 | |||
492 | /** |
||
493 | * @param int $type |
||
494 | * @param int $flags |
||
495 | * @return static |
||
496 | */ |
||
497 | public function withSortQuery(int $type = SORT_DESC, int $flags = SORT_REGULAR) |
||
498 | { |
||
499 | 1 | $array = $this->getQueryArray(); |
|
500 | |||
501 | 1 | if (SORT_DESC === $type) { |
|
502 | krsort($array, $flags); |
||
503 | } else { |
||
504 | ksort($array, $flags); |
||
505 | 1 | } |
|
506 | |||
507 | return $this->withQueryArray($array); |
||
508 | } |
||
509 | 1 | ||
510 | 1 | public function getRawQuery(): string |
|
511 | 1 | { |
|
512 | $items = []; |
||
513 | 1 | foreach ($this->getQueryArray() as $name => $value) { |
|
514 | $items[] = "$name=$value"; |
||
515 | 1 | } |
|
516 | return implode('&', $items); |
||
517 | } |
||
518 | |||
519 | /** |
||
520 | * Check if host is matched. |
||
521 | 2 | * |
|
522 | * @param string $pattern |
||
523 | 2 | * |
|
524 | * @return bool |
||
525 | */ |
||
526 | public function matchHost($pattern) |
||
527 | { |
||
528 | if (empty($pattern) || empty($this->getHost())) { |
||
529 | return false; |
||
530 | } |
||
531 | 8 | ||
532 | if ($pattern == $this->getHost()) { |
||
533 | 8 | return true; |
|
534 | } |
||
535 | |||
536 | 8 | $pattern = preg_quote($pattern, '#'); |
|
537 | 6 | $pattern = str_replace('\*', '.*', $pattern); |
|
538 | $pattern = str_replace('\|', '|', $pattern); |
||
539 | |||
540 | 8 | $pattern = '#^('.$pattern.')\z#u'; |
|
541 | 8 | ||
542 | return 1 == preg_match($pattern, $this->getHost()); |
||
543 | } |
||
544 | |||
545 | /** |
||
546 | * {@inheritdoc} |
||
547 | */ |
||
548 | public function __toString() |
||
549 | { |
||
550 | return $this->toString(); |
||
551 | 12 | } |
|
552 | |||
553 | 12 | /** |
|
554 | * Is a given port non-standard for the current scheme? |
||
555 | * |
||
556 | * @return bool |
||
557 | */ |
||
558 | private function isNonStandardPort() |
||
559 | { |
||
560 | if (!$this->scheme && $this->port) { |
||
0 ignored issues
–
show
The expression
$this->port of type integer|null is loosely compared to true ; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||
561 | return true; |
||
562 | } |
||
563 | if (!$this->host || !$this->port) { |
||
0 ignored issues
–
show
The expression
$this->port of type integer|null is loosely compared to false ; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||
564 | return false; |
||
565 | } |
||
566 | |||
567 | return !isset($this->schemes[$this->scheme]) |
||
568 | || $this->port !== $this->schemes[$this->scheme]; |
||
569 | } |
||
570 | |||
571 | /** |
||
572 | * is url string. |
||
573 | * |
||
574 | * @param mixed $url |
||
575 | * |
||
576 | * @return bool |
||
577 | */ |
||
578 | public static function isUrlString($url) |
||
579 | { |
||
580 | return false !== filter_var($url, FILTER_VALIDATE_URL); |
||
581 | } |
||
582 | } |
||
583 |