Passed
Push — master ( 3a5683...386b65 )
by Terry
01:50
created

src/Psr7/Uri.php (1 issue)

1
<?php 
2
/*
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace Shieldon\Psr7;
14
15
use Psr\Http\Message\UriInterface;
16
use InvalidArgumentException;
17
18
use function filter_var;
19
use function gettype;
20
use function is_integer;
21
use function is_null;
22
use function is_string;
23
use function ltrim;
24
use function parse_url;
25
use function rawurlencode;
26
use function sprintf;
27
28
/*
29
 * Value object representing a URI.
30
 */
31
class Uri implements UriInterface
32
{
33
    /**
34
     *    foo://example.com:8042/over/there?name=ferret#nose
35
     *    \_/   \______________/\_________/ \_________/ \__/
36
     *     |           |            |            |        |
37
     *  scheme     authority       path        query   fragment
38
     */
39
40
    /**
41
     * The scheme component of the URI.
42
     * For example, https://terryl.in/
43
     * In this case, "https" is the scheme.
44
     * 
45
     * @var string
46
     */
47
    protected $scheme;
48
49
    /**
50
     * The user component of the URI.
51
     * For example, https://jack:[email protected]
52
     * In this case, "jack" is the user.
53
     *
54
     * @var string
55
     */
56
    protected $user;
57
58
    /**
59
     * The password component of the URI.
60
     * For example, http://jack:[email protected]
61
     * In this case, "1234" is the password.
62
     *
63
     * @var string
64
     */
65
    protected $pass;
66
67
    /**
68
     * The host component of the URI.
69
     * For example, https://terryl.in:443/zh/
70
     * In this case, "terryl.in" is the host.
71
     *
72
     * @var string
73
     */
74
    protected $host;
75
76
    /**
77
     * The port component of the URI.
78
     * For example, https://terryl.in:443
79
     * In this case, "443" is the port.
80
     * 
81
     * @var int|null
82
     */
83
    protected $port;
84
85
    /**
86
     * The path component of the URI.
87
     * For example, https://terryl.in/zh/?paged=2
88
     * In this case, "/zh/" is the path.
89
     *
90
     * @var string
91
     */
92
    protected $path;
93
94
    /**
95
     * The query component of the URI.
96
     * For example, https://terryl.in/zh/?paged=2
97
     * In this case, "paged=2" is the query.
98
     *
99
     * @var string
100
     */
101
    protected $query;
102
103
    /**
104
     * The fragment component of the URI.
105
     * For example, https://terryl.in/#main-container
106
     * In this case, "main-container" is the fragment.
107
     *
108
     * @var string
109
     */
110
    protected $fragment;
111
112
    /**
113
     * Uri constructor.
114
     * 
115
     * @param string $uri The URI.
116
     */
117
    public function __construct($uri = '')
118
    {
119
        $this->init();
120
121
        if ($uri !== '') {
122
            $this->assertValidUri($uri);
123
            $this->init(parse_url($uri));
124
        }
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    public function getScheme(): string
131
    {
132
        return $this->scheme;
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function getAuthority(): string
139
    {
140
        $authority = '';
141
142
        if ($this->getUserInfo()) {
143
            $authority .= $this->getUserInfo() . '@';
144
        }
145
146
        $authority .= $this->getHost();
147
148
        if (! empty($this->getPort())) {
149
            $authority .= ':' . $this->getPort();
150
        }
151
152
        return $authority;
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function getUserInfo(): string
159
    {
160
        $userInfo = $this->user;
161
162
        if (! empty($this->pass)) {
163
            $userInfo .= ':' . $this->pass;
164
        }
165
166
        return $userInfo;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function getHost(): string
173
    {
174
        return $this->host;
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function getPort()
181
    {
182
        return $this->port;
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188
    public function getPath(): string
189
    {
190
        return $this->path;
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    public function getQuery(): string
197
    {
198
        return $this->query;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    public function getFragment(): string
205
    {
206
        return $this->fragment;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function withScheme($scheme)
213
    {
214
        $this->assertScheme($scheme);
215
216
        $scheme = $this->filter('scheme', $scheme);
217
218
        $clone = clone $this;
219
        $clone->scheme = $scheme;
220
        return $clone;
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     */
226
    public function withUserInfo($user, $pass = null)
227
    {
228
        $this->assertString($user, 'user');
229
        $user = $this->filter('user', $user);
230
231
        if ($pass) {
232
            $this->assertString($pass, 'pass');
233
            $pass = $this->filter('pass', $pass);
234
        }
235
236
        $clone = clone $this;
237
        $clone->user = $user;
238
        $clone->pass = $pass;
239
240
        return $clone;
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246
    public function withHost($host)
247
    {
248
        $this->assertHost($host);
249
250
        $host = $this->filter('host', $host);
251
252
        $clone = clone $this;
253
        $clone->host = $host;
254
255
        return $clone;
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261
    public function withPort($port)
262
    {
263
        $this->assertPort($port);
264
265
        $port = $this->filter('port', $port);
266
267
        $clone = clone $this;
268
        $clone->port = $port;
0 ignored issues
show
Documentation Bug introduced by
It seems like $port of type string is incompatible with the declared type integer|null of property $port.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
269
270
        return $clone;
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     */
276
    public function withPath($path)
277
    {
278
        $this->assertString($path, 'path');
279
280
        $path = $this->filter('path', $path);
281
282
        $clone = clone $this;
283
        $clone->path = '/' . rawurlencode(ltrim($path, '/'));
284
285
        return $clone;
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    public function withQuery($query)
292
    {
293
        $this->assertString($query, 'query');
294
295
        $query = $this->filter('query', $query);
296
297
        // & => %26
298
        // ? => %3F
299
300
        $clone = clone $this;
301
        $clone->query = $query;
302
303
        return $clone;
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    public function withFragment($fragment)
310
    {
311
        $this->assertString($fragment, 'fragment');
312
313
        $fragment = $this->filter('fragment', $fragment);
314
315
        $clone = clone $this;
316
        $clone->fragment = rawurlencode($fragment);
317
318
        return $clone;
319
    }
320
321
    /**
322
     * {@inheritdoc}
323
     */
324
    public function __toString(): string
325
    {
326
        $uri = '';
327
328
        // If a scheme is present, it MUST be suffixed by ":".
329
        if ($this->getScheme()) {
330
            $uri .= $this->getScheme() . ':';
331
        }
332
333
        // If an authority is present, it MUST be prefixed by "//".
334
        if ($this->getAuthority()) {
335
            $uri .= '//' . $this->getAuthority();
336
        }
337
338
        // If the path is rootless and an authority is present, the path MUST
339
        // be prefixed by "/".
340
        $uri .= '/' . ltrim($this->getPath(), '/');
341
342
        // If a query is present, it MUST be prefixed by "?".
343
        if ($this->getQuery()) {
344
            $uri .= '?' . $this->getQuery();
345
        }
346
347
        // If a fragment is present, it MUST be prefixed by "#".
348
        if ($this->getFragment()) {
349
            $uri .= '#' . $this->getFragment();
350
        }
351
352
        return $uri;
353
    }
354
355
    /*
356
    |--------------------------------------------------------------------------
357
    | Non PSR-7 Methods.
358
    |--------------------------------------------------------------------------
359
    */
360
361
    /**
362
     * Initialize.
363
     *
364
     * @param array $data Parsed URL data.
365
     *
366
     * @return void
367
     */
368
    protected function init(array $data = []): void
369
    {
370
        $components = [
371
            'scheme', 
372
            'user', 
373
            'pass',
374
            'host',
375
            'port',
376
            'path', 
377
            'query', 
378
            'fragment'
379
        ];
380
381
        foreach($components as $v) {
382
            $this->{$v} = isset($data[$v]) ? $this->filter($v, $data[$v]) : '';
383
        }
384
385
        // According to PSR-7, return null or int for the URI port.
386
        $this->port = isset($data['port']) ? (int) $data['port'] : null;
387
    }
388
389
    /**
390
     * Filter URI components.
391
     * 
392
     * Users can provide both encoded and decoded characters.
393
     * Implementations ensure the correct encoding as outlined.
394
     * @see https://tools.ietf.org/html/rfc3986#section-2.2
395
     *
396
     * @param string          $key
397
     * @param string|int|null $value
398
     *
399
     * @return string
400
     */
401
    protected function filter(string $key, $value)
402
    {
403
        // gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
404
        // $genDelims = ':/\?#\[\]@';
405
 
406
        // sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
407
        //             / "*" / "+" / "," / ";" / "="
408
        $subDelims = '!\$&\'\(\)\*\+,;=';
409
410
        // $unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
411
        $unReserved = 'a-zA-Z0-9\-\._~';
412
413
        // Encoded characters, such as "?" encoded to "%3F".
414
        $encodePattern = '%(?![A-Fa-f0-9]{2})';
415
416
        $regex = '';
417
418
        switch ($key) {
419
420
            case 'query':
421
            case 'fragment':
422
                $specPattern = '%:@\/\?';
423
                $regex = '/(?:[^' . $unReserved . $subDelims . $specPattern . ']+|' . $encodePattern . ')/';
424
                break;
425
426
            case 'path':
427
                $specPattern = '%:@\/';
428
                $regex = '/(?:[^' . $unReserved . $subDelims . $specPattern . ']+|' . $encodePattern . ')/';
429
                break;
430
431
            case 'user':
432
            case 'pass':
433
                $regex = '/(?:[^%' . $unReserved . $subDelims . ']+|' . $encodePattern . ')/';
434
                break;
435
436
            default:
437
        }
438
439
        if ($regex) {
440
            return preg_replace_callback(
441
                $regex,
442
                function ($match) {
443
                    return rawurlencode($match[0]);
444
                },
445
                $value
446
            );
447
        }
448
449
        return $value;
450
    }
451
452
    /**
453
     * Throw exception for the invalid scheme.
454
     *
455
     * @param string $scheme The scheme string of a URI.
456
     *
457
     * @return void
458
     * 
459
     * @throws InvalidArgumentException
460
     */
461
    protected function assertScheme($scheme): void
462
    {
463
        $this->assertString($scheme, 'scheme');
464
465
        $validSchemes = [
466
            0 => '',
467
            1 => 'http',
468
            2 => 'https',
469
        ];
470
471
        if (! in_array($scheme, $validSchemes)) {
472
            throw new InvalidArgumentException(
473
                sprintf(
474
                    'The string "%s" is not a valid scheme.',
475
                    $scheme
476
                )
477
            );
478
        }
479
    }
480
481
    /**
482
     * Throw exception for the invalid value.
483
     *
484
     * @param string $value The value to check.
485
     * @param string $name  The name of the value.
486
     *
487
     * @return void
488
     * 
489
     * @throws InvalidArgumentException
490
     */
491
    protected function assertString($value, string $name = 'it'): void
492
    {
493
        if (! is_string($value)) {
494
            throw new InvalidArgumentException(
495
                sprintf(
496
                    ucfirst($name) . ' must be a string, but %s provided.',
497
                    gettype($value)
498
                )
499
            );
500
        }
501
    }
502
503
    /**
504
     * Throw exception for the invalid URI string.
505
     *
506
     * @param string $uri The URI string.
507
     * 
508
     * @return void
509
     * 
510
     * @throws InvalidArgumentException
511
     */
512
    protected function assertValidUri($uri): void
513
    {
514
        $this->assertString($uri, 'uri');
515
516
        if (! filter_var($uri, FILTER_VALIDATE_URL)) {
517
            throw new InvalidArgumentException(
518
                sprintf(
519
                    '"%s" is not a valid URI',
520
                    $uri
521
                )
522
            );
523
        }
524
    }
525
526
    /**
527
     * Throw exception for the invalid host string.
528
     *
529
     * @param string $host The host string to of a URI.
530
     * 
531
     * @return void
532
     * 
533
     * @throws InvalidArgumentException
534
     */
535
    protected function assertHost($host): void
536
    {
537
        $this->assertString($host);
538
539
        if ($host === '') {
540
            // Note: An empty host value is equivalent to removing the host.
541
            // So that if the host is empty, ignore the following check.
542
            return;
543
        }
544
545
        if (! filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
546
            throw new InvalidArgumentException(
547
                sprintf(
548
                    '"%s" is not a valid host',
549
                    $host
550
                )
551
            );
552
        }
553
    }
554
555
    /**
556
     * Throw exception for the invalid port.
557
     *
558
     * @param null|int $port The port number to of a URI.
559
     * 
560
     * @return void
561
     *
562
     * @throws InvalidArgumentException
563
     */
564
    protected function assertPort($port): void
565
    {
566
        if (
567
            ! is_null($port) && 
568
            ! is_integer($port)
569
        ) {
570
            throw new InvalidArgumentException(
571
                sprintf(
572
                    'Port must be an integer or a null value, but %s provided.',
573
                    gettype($port)
574
                )
575
            );
576
        }
577
578
        if (! ($port > 0 && $port < 65535)) {
579
            throw new InvalidArgumentException(
580
                sprintf(
581
                    'Port number should be in a range of 0-65535, but %s provided.',
582
                    $port
583
                )
584
            );
585
        }
586
    }
587
}
588