Issues (3)

src/Url.php (3 issues)

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
Consider adding a comment why this CATCH block is empty.
Loading history...
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
Bug Best Practice introduced by
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 ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

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
Loading history...
561
            return true;
562
        }
563
        if (!$this->host || !$this->port) {
0 ignored issues
show
Bug Best Practice introduced by
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 ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

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
Loading history...
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