Passed
Branch master (d2bd53)
by hugh
13:28
created

Url::toString()   B

Complexity

Conditions 6
Paths 32

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 31
c 0
b 0
f 0
ccs 18
cts 18
cp 1
rs 8.8017
cc 6
nc 32
nop 0
crap 6
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
    /**
19
     * @var string|null url scheme
20
     */
21
    private $scheme;
22
23
    /**
24
     * @var string|null url host
25
     */
26
    private $host;
27
28
    /**
29
     * @var int|null url port
30
     */
31
    private $port;
32
33
    /**
34
     * @var string|null url user
35
     */
36
    private $user;
37
38
    /**
39
     * @var string|null url pass
40
     */
41
    private $pass;
42
43
    /**
44
     * @var string|null url path
45
     */
46
    private $path;
47
48
    /**
49
     * @var string|null url query string
50
     */
51
    private $query;
52
53
    /**
54
     * @var string|null url fragment
55
     */
56
    private $fragment;
57
58
    /**
59
     * 获取实例.
60
     *
61
     * @param null|UriInterface $url
62
     *
63
     * @return static
64
     */
65 12
    public static function instance($url = null)
66
    {
67 12
        return new static($url);
68
    }
69
70
    /**
71
     * Url constructor.
72
     *
73
     * @param null|string|string[]|UriInterface $url
74
     */
75 12
    final protected function __construct($url = null)
76
    {
77 12
        if ($url instanceof UriInterface) {
78 4
            $this->parsePsrUrl($url);
79 12
        } elseif (is_string($url)) {
80 9
            $this->parseStringUrl($url);
81 5
        } elseif (is_array($url)) {
82 4
            $this->parseArrayUrl($url);
83
        }
84 11
    }
85
86
    /**
87
     * 解析 Psr 标准库的url.
88
     *
89
     * @param UriInterface $url
90
     *
91
     * @return $this
92
     */
93 4
    private function parsePsrUrl(UriInterface $url)
94
    {
95 4
        $this->scheme = empty($scheme = $url->getScheme()) ? null : $scheme;
96 4
        $this->host = empty($host = $url->getHost()) ? null : $host;
97 4
        $this->port = empty($port = $url->getPort()) ? null : $port;
98 4
        $this->path = empty($path = $url->getPath()) ? null : $path;
99 4
        $this->query = empty($query = $url->getQuery()) ? null : $query;
100 4
        $this->fragment = empty($fragment = $url->getFragment()) ? null : $fragment;
101
102 4
        $user = $this->getUserInfo();
103 4
        $user = explode(':', $user);
104 4
        $this->user = (is_array($user) && isset($user[0])) ? $user[0] : null;
105 4
        $this->pass = (is_array($user) && isset($user[1])) ? $user[1] : null;
106
107 4
        return $this;
108
    }
109
110
    /**
111
     * 解析字符串url.
112
     *
113
     * @param string $url
114
     *
115
     * @return $this
116
     */
117 9
    private function parseStringUrl($url)
118
    {
119 9
        if (!static::isUrlString($url)) {
120 1
            throw new InvalidArgumentException('the parameter must be a url');
121
        }
122
123
        /** @var string[] $parts */
124 8
        $parts = parse_url($url);
125 8
        $this->parseArrayUrl($parts);
126
127 8
        return $this;
128
    }
129
130
    /**
131
     * 解析数组url.
132
     *
133
     * @param string[]|int[] $parts
134
     *
135
     * @return $this
136
     */
137 10
    private function parseArrayUrl(array $parts)
138
    {
139 10
        $this->scheme = isset($parts['scheme']) ? $parts['scheme'] : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like isset($parts['scheme']) ? $parts['scheme'] : null can also be of type integer. However, the property $scheme is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
140 10
        $this->host = isset($parts['host']) ? $parts['host'] : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like isset($parts['host']) ? $parts['host'] : null can also be of type integer. However, the property $host is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
141 10
        $this->port = isset($parts['port']) ? $parts['port'] : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like isset($parts['port']) ? $parts['port'] : null can also be of type string. However, the property $port is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
142 10
        $this->user = isset($parts['user']) ? $parts['user'] : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like isset($parts['user']) ? $parts['user'] : null can also be of type integer. However, the property $user is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
143 10
        $this->pass = isset($parts['pass']) ? $parts['pass'] : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like isset($parts['pass']) ? $parts['pass'] : null can also be of type integer. However, the property $pass is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
144 10
        $this->path = isset($parts['path']) ? $parts['path'] : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like isset($parts['path']) ? $parts['path'] : null can also be of type integer. However, the property $path is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
145 10
        $this->query = isset($parts['query']) ? $parts['query'] : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like isset($parts['query']) ? $parts['query'] : null can also be of type integer. However, the property $query is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
146 10
        $this->fragment = isset($parts['fragment']) ? $parts['fragment'] : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like isset($parts['fragment']...arts['fragment'] : null can also be of type integer. However, the property $fragment is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
147
148 10
        return $this;
149
    }
150
151
    /**
152
     * 填充 Psr 标准库的url.
153
     *
154
     * @param UriInterface $url
155
     *
156
     * @return UriInterface
157
     */
158
    public function fillPsrUri(UriInterface $url)
159
    {
160
        return $url->withScheme($this->getScheme())
161
            ->withUserInfo($this->getUser(), $this->getPass())
162
            ->withHost($this->getHost())
163
            ->withPort($this->getPort())
164
            ->withPath($this->getPath())
165
            ->withQuery($this->getQuery())
166
            ->withFragment($this->getFragment());
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172 10
    public function getScheme()
173
    {
174 10
        return strval($this->scheme);
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180 8
    public function getAuthority()
181
    {
182 8
        $authority = $host = $this->getHost();
183 8
        if (empty($host)) {
184
            return $authority;
185
        }
186
187 8
        $userInfo = $this->getUserInfo();
188 8
        if (!empty($userInfo)) {
189
            $authority = "{$userInfo}@{$authority}";
190
        }
191
192 8
        $port = $this->getPort();
193 8
        if ($this->isNonStandardPort() && !empty($port)) {
194
            $authority = "{$authority}:{$port}";
195
        }
196
197 8
        return $authority;
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203 8
    public function getUserInfo()
204
    {
205 8
        $userInfo = $user = $this->getUser();
206 8
        if (empty($user)) {
207 8
            return $userInfo;
208
        }
209
210
        $pass = $this->getPass();
211
        if (!empty($pass)) {
212
            $userInfo = "{$userInfo}:{$pass}";
213
        }
214
215
        return $userInfo;
216
    }
217
218
    /**
219
     * 获取 url user.
220
     *
221
     * @return string
222
     */
223 8
    public function getUser()
224
    {
225 8
        return strval($this->user);
226
    }
227
228
    /**
229
     * 获取 url pass.
230
     *
231
     * @return string
232
     */
233 2
    public function getPass()
234
    {
235 2
        return strval($this->pass);
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241 8
    public function getHost()
242
    {
243 8
        return strval($this->host);
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249 8
    public function getPort()
250
    {
251 8
        if (!empty($this->port)) {
252 4
            return $this->port;
253
        }
254
255 8
        $scheme = $this->getScheme();
256
257 8
        return isset($this->schemes[$scheme]) ? $this->schemes[$scheme] : null;
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263 8
    public function getPath()
264
    {
265 8
        if (empty($this->path)) {
266
            return '';
267
        }
268
269 8
        return '/' === substr($this->path, 0, 1) ? $this->path : "/{$this->path}";
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275 8
    public function getQuery()
276
    {
277 8
        return strval($this->query);
278
    }
279
280
    /**
281
     * 获取query数组.
282
     *
283
     * @return array
284
     */
285 2
    public function getQueryArray()
286
    {
287 2
        $query = $this->getQuery();
288
289 2
        $queryArray = [];
290 2
        if (!empty($query)) {
291 2
            parse_str($query, $queryArray);
292
        }
293
294 2
        return is_array($queryArray) ? $queryArray : [];
295
    }
296
297
    /**
298
     * 是否存在query的key.
299
     *
300
     * @param string $key
301
     *
302
     * @return bool
303
     */
304
    public function hasQueryKey($key)
305
    {
306
        $queryArray = $this->getQueryArray();
307
308
        return array_key_exists($key, $queryArray);
309
    }
310
311
    /**
312
     * 是否存在query的key.
313
     *
314
     * @param string $key
315
     * @param mixed  $default
316
     *
317
     * @return array|string
318
     */
319
    public function getQueryValue($key, $default = null)
320
    {
321
        $queryArray = $this->getQueryArray();
322
323
        return array_key_exists($key, $queryArray) ? $queryArray[$key] : $default;
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329 8
    public function getFragment()
330
    {
331 8
        return strval($this->fragment);
332
    }
333
334
    /**
335
     * Return the string representation as a URI reference.
336
     *
337
     * @return string
338
     */
339 8
    public function toString()
340
    {
341 8
        $url = '';
342
343 8
        $scheme = $this->getScheme();
344 8
        if (!empty($scheme)) {
345 8
            $url = "{$scheme}://{$url}";
346
        }
347
348 8
        $authority = $this->getAuthority();
349 8
        if (!empty($authority)) {
350 8
            $url = "{$url}{$authority}";
351
        }
352
353 8
        $path = $this->getPath();
354 8
        if (!empty($path)) {
355 8
            $url = "{$url}{$path}";
356
        }
357
358 8
        $query = $this->getQuery();
359 8
        if (!empty($query)) {
360 8
            $url = "{$url}?{$query}";
361
        }
362
363 8
        $fragment = $this->getFragment();
364 8
        if (!empty($fragment)) {
365 4
            $url = "{$url}#{$fragment}";
366
        }
367
368 8
        return $url;
369
    }
370
371
    /**
372
     * {@inheritdoc}
373
     */
374
    public function withScheme($scheme)
375
    {
376
        $new = clone $this;
377
        $new->scheme = $scheme;
378
379
        return $new;
380
    }
381
382
    /**
383
     * {@inheritdoc}
384
     */
385
    public function withUserInfo($user, $password = null)
386
    {
387
        $new = clone $this;
388
        $new->user = $user;
389
        $new->pass = $password;
390
391
        return $new;
392
    }
393
394
    /**
395
     * {@inheritdoc}
396
     */
397
    public function withHost($host)
398
    {
399
        $new = clone $this;
400
        $new->host = $host;
401
402
        return $new;
403
    }
404
405
    /**
406
     * {@inheritdoc}
407
     */
408
    public function withPort($port)
409
    {
410
        $new = clone $this;
411
        $new->port = $port;
412
413
        return $new;
414
    }
415
416
    /**
417
     * {@inheritdoc}
418
     */
419
    public function withPath($path)
420
    {
421
        $new = clone $this;
422
        $new->path = $path;
423
424
        return $new;
425
    }
426
427
    /**
428
     * {@inheritdoc}
429
     */
430
    public function withQuery($query)
431
    {
432
        $new = clone $this;
433
        $new->query = $query;
434
435
        return $new;
436
    }
437
438
    /**
439
     * Return an instance with the specified query array.
440
     *
441
     * @param array $queryArray
442
     *
443
     * @return static
444
     */
445
    public function withQueryArray(array $queryArray)
446
    {
447
        return $this->withQuery(http_build_query($queryArray));
448
    }
449
450
    /**
451
     * Create a new URI with a specific query string value removed.
452
     *
453
     * @param string|int $key
454
     *
455
     * @return static
456
     */
457
    public function withoutQueryValue($key)
458
    {
459
        $queryArray = $this->getQueryArray();
460
461
        if (isset($queryArray[$key])) {
462
            unset($queryArray[$key]);
463
        }
464
465
        return $this->withQueryArray($queryArray);
466
    }
467
468
    /**
469
     * Create a new URI with a specific query string value.
470
     *
471
     * @param string     $key
472
     * @param string|int $value
473
     *
474
     * @return static
475
     */
476
    public function withQueryValue($key, $value)
477
    {
478
        $queryArray = $this->getQueryArray();
479
        $queryArray[$key] = $value;
480
481
        return $this->withQueryArray($queryArray);
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     */
487
    public function withFragment($fragment)
488
    {
489
        $new = clone $this;
490
        $new->fragment = $fragment;
491
492
        return $new;
493
    }
494
495
    /**
496
     * {@inheritdoc}
497
     */
498 2
    public function __toString()
499
    {
500 2
        return $this->toString();
501
    }
502
503
    /**
504
     * Is a given port non-standard for the current scheme?
505
     *
506
     * @return bool
507
     */
508 8
    private function isNonStandardPort()
509
    {
510 8
        if (!$this->scheme && $this->port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->scheme of type string|null is loosely compared to false; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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 zero. 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...
511
            return true;
512
        }
513
514 8
        if (!$this->host || !$this->port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->host of type string|null is loosely compared to false; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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 zero. 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...
515 6
            return false;
516
        }
517
518 4
        return !isset($this->schemes[$this->scheme])
519 4
            || $this->port !== $this->schemes[$this->scheme];
520
    }
521
522
    /**
523
     * is url string.
524
     *
525
     * @param mixed $url
526
     *
527
     * @return bool
528
     */
529 9
    public static function isUrlString($url)
530
    {
531 9
        return false !== filter_var($url, FILTER_VALIDATE_URL);
532
    }
533
}
534