Completed
Push — master ( 17fe97...d06155 )
by Tobias
02:00
created

Uri::fromParts()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
3
declare (strict_types = 1);
4
5
namespace Nyholm\Psr7;
6
7
use Psr\Http\Message\UriInterface;
8
9
/**
10
 * PSR-7 URI implementation.
11
 *
12
 * @author Michael Dowling
13
 * @author Tobias Schultze
14
 * @author Matthew Weier O'Phinney
15
 * @author Tobias Nyholm <[email protected]>
16
 */
17
class Uri implements UriInterface
18
{
19
    private static $schemes = [
20
        'http' => 80,
21
        'https' => 443,
22
    ];
23
24
    private static $charUnreserved = 'a-zA-Z0-9_\-\.~';
25
    private static $charSubDelims = '!\$&\'\(\)\*\+,;=';
26
    private static $replaceQuery = ['=' => '%3D', '&' => '%26'];
27
28
    /**
29
     * @var string Uri scheme.
30
     */
31
    private $scheme = '';
32
33
    /**
34
     * @var string Uri user info.
35
     */
36
    private $userInfo = '';
37
38
    /**
39
     * @var string Uri host.
40
     */
41
    private $host = '';
42
43
    /**
44
     * @var int|null Uri port.
45
     */
46
    private $port;
47
48
    /**
49
     * @var string Uri path.
50
     */
51
    private $path = '';
52
53
    /**
54
     * @var string Uri query string.
55
     */
56
    private $query = '';
57
58
    /**
59
     * @var string Uri fragment.
60
     */
61
    private $fragment = '';
62
63
    /**
64
     * @param string $uri
65
     */
66
    public function __construct($uri = '')
67
    {
68
        if ($uri != '') {
69
            $parts = parse_url($uri);
70
            if ($parts === false) {
71
                throw new \InvalidArgumentException("Unable to parse URI: $uri");
72
            }
73
74
            $this->applyParts($parts);
75
        }
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81
    public function __toString(): string
82
    {
83
        return self::createUriString(
84
            $this->scheme,
85
            $this->getAuthority(),
86
            $this->path,
87
            $this->query,
88
            $this->fragment
89
        );
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95
    public function getScheme(): string
96
    {
97
        return $this->scheme;
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function getAuthority(): string
104
    {
105
        if ($this->host == '') {
106
            return '';
107
        }
108
109
        $authority = $this->host;
110
        if ($this->userInfo != '') {
111
            $authority = $this->userInfo.'@'.$authority;
112
        }
113
114
        if ($this->port !== null) {
115
            $authority .= ':'.$this->port;
116
        }
117
118
        return $authority;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function getUserInfo(): string
125
    {
126
        return $this->userInfo;
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132
    public function getHost(): string
133
    {
134
        return $this->host;
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function getPort()
141
    {
142
        return $this->port;
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148
    public function getPath(): string
149
    {
150
        return $this->path;
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    public function getQuery(): string
157
    {
158
        return $this->query;
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function getFragment(): string
165
    {
166
        return $this->fragment;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function withScheme($scheme): self
173
    {
174
        $scheme = $this->filterScheme($scheme);
175
176
        if ($this->scheme === $scheme) {
177
            return $this;
178
        }
179
180
        $new = clone $this;
181
        $new->scheme = $scheme;
182
        $new->port = $new->filterPort($new->port);
183
184
        return $new;
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190
    public function withUserInfo($user, $password = null): self
191
    {
192
        $info = $user;
193
        if ($password != '') {
194
            $info .= ':'.$password;
195
        }
196
197
        if ($this->userInfo === $info) {
198
            return $this;
199
        }
200
201
        $new = clone $this;
202
        $new->userInfo = $info;
203
204
        return $new;
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function withHost($host): self
211
    {
212
        $host = $this->filterHost($host);
213
214
        if ($this->host === $host) {
215
            return $this;
216
        }
217
218
        $new = clone $this;
219
        $new->host = $host;
220
221
        return $new;
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227
    public function withPort($port): self
228
    {
229
        $port = $this->filterPort($port);
230
231
        if ($this->port === $port) {
232
            return $this;
233
        }
234
235
        $new = clone $this;
236
        $new->port = $port;
237
238
        return $new;
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     */
244
    public function withPath($path): self
245
    {
246
        $path = $this->filterPath($path);
247
248
        if ($this->path === $path) {
249
            return $this;
250
        }
251
252
        $new = clone $this;
253
        $new->path = $path;
254
255
        return $new;
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261
    public function withQuery($query): self
262
    {
263
        $query = $this->filterQueryAndFragment($query);
264
265
        if ($this->query === $query) {
266
            return $this;
267
        }
268
269
        $new = clone $this;
270
        $new->query = $query;
271
272
        return $new;
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278
    public function withFragment($fragment): self
279
    {
280
        $fragment = $this->filterQueryAndFragment($fragment);
281
282
        if ($this->fragment === $fragment) {
283
            return $this;
284
        }
285
286
        $new = clone $this;
287
        $new->fragment = $fragment;
288
289
        return $new;
290
    }
291
292
    /**
293
     * Apply parse_url parts to a URI.
294
     *
295
     * @param array $parts Array of parse_url parts to apply.
296
     */
297
    private function applyParts(array $parts)
298
    {
299
        $this->scheme = isset($parts['scheme'])
300
            ? $this->filterScheme($parts['scheme'])
301
            : '';
302
        $this->userInfo = isset($parts['user']) ? $parts['user'] : '';
303
        $this->host = isset($parts['host'])
304
            ? $this->filterHost($parts['host'])
305
            : '';
306
        $this->port = isset($parts['port'])
307
            ? $this->filterPort($parts['port'])
308
            : null;
309
        $this->path = isset($parts['path'])
310
            ? $this->filterPath($parts['path'])
311
            : '';
312
        $this->query = isset($parts['query'])
313
            ? $this->filterQueryAndFragment($parts['query'])
314
            : '';
315
        $this->fragment = isset($parts['fragment'])
316
            ? $this->filterQueryAndFragment($parts['fragment'])
317
            : '';
318
        if (isset($parts['pass'])) {
319
            $this->userInfo .= ':'.$parts['pass'];
320
        }
321
    }
322
323
    /**
324
     * Create a URI string from its various parts.
325
     *
326
     * @param string $scheme
327
     * @param string $authority
328
     * @param string $path
329
     * @param string $query
330
     * @param string $fragment
331
     *
332
     * @return string
333
     */
334
    private static function createUriString($scheme, $authority, $path, $query, $fragment): string
335
    {
336
        $uri = '';
337
338
        if ($scheme != '') {
339
            $uri .= $scheme.':';
340
        }
341
342
        if ($authority != '') {
343
            $uri .= '//'.$authority;
344
        }
345
346
        if ($path != '') {
347
            if ($path[0] !== '/') {
348
                if ($authority != '') {
349
                    // If the path is rootless and an authority is present, the path MUST be prefixed by "/"
350
                    $path = '/'.$path;
351
                }
352
            } elseif (isset($path[1]) && $path[1] === '/') {
353
                if ($authority == '') {
354
                    // If the path is starting with more than one "/" and no authority is present, the
355
                    // starting slashes MUST be reduced to one.
356
                    $path = '/'.ltrim($path, '/');
357
                }
358
            }
359
360
            $uri .= $path;
361
        }
362
363
        if ($query != '') {
364
            $uri .= '?'.$query;
365
        }
366
367
        if ($fragment != '') {
368
            $uri .= '#'.$fragment;
369
        }
370
371
        return $uri;
372
    }
373
374
    /**
375
     * Is a given port non-standard for the current scheme?
376
     *
377
     * @param string $scheme
378
     * @param int    $port
379
     *
380
     * @return bool
381
     */
382
    private static function isNonStandardPort($scheme, $port): bool
383
    {
384
        return !isset(self::$schemes[$scheme]) || $port !== self::$schemes[$scheme];
385
    }
386
387
    /**
388
     * @param string $scheme
389
     *
390
     * @return string
391
     *
392
     * @throws \InvalidArgumentException If the scheme is invalid.
393
     */
394
    private function filterScheme($scheme): string
395
    {
396
        if (!is_string($scheme)) {
397
            throw new \InvalidArgumentException('Scheme must be a string');
398
        }
399
400
        return strtolower($scheme);
401
    }
402
403
    /**
404
     * @param string $host
405
     *
406
     * @return string
407
     *
408
     * @throws \InvalidArgumentException If the host is invalid.
409
     */
410
    private function filterHost($host): string
411
    {
412
        if (!is_string($host)) {
413
            throw new \InvalidArgumentException('Host must be a string');
414
        }
415
416
        return strtolower($host);
417
    }
418
419
    /**
420
     * @param int|null $port
421
     *
422
     * @return int|null
423
     *
424
     * @throws \InvalidArgumentException If the port is invalid.
425
     */
426
    private function filterPort($port)
427
    {
428
        if ($port === null) {
429
            return;
430
        }
431
432
        $port = (int) $port;
433
        if (1 > $port || 0xffff < $port) {
434
            throw new \InvalidArgumentException(
435
                sprintf('Invalid port: %d. Must be between 1 and 65535', $port)
436
            );
437
        }
438
439
        return self::isNonStandardPort($this->scheme, $port) ? $port : null;
440
    }
441
442
    /**
443
     * Filters the path of a URI.
444
     *
445
     * @param string $path
446
     *
447
     * @return string
448
     *
449
     * @throws \InvalidArgumentException If the path is invalid.
450
     */
451 View Code Duplication
    private function filterPath($path): string
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
452
    {
453
        if (!is_string($path)) {
454
            throw new \InvalidArgumentException('Path must be a string');
455
        }
456
457
        return preg_replace_callback(
458
            '/(?:[^'.self::$charUnreserved.self::$charSubDelims.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
459
            [$this, 'rawurlencodeMatchZero'],
460
            $path
461
        );
462
    }
463
464
    /**
465
     * Filters the query string or fragment of a URI.
466
     *
467
     * @param string $str
468
     *
469
     * @return string
470
     *
471
     * @throws \InvalidArgumentException If the query or fragment is invalid.
472
     */
473 View Code Duplication
    private function filterQueryAndFragment($str): string
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
474
    {
475
        if (!is_string($str)) {
476
            throw new \InvalidArgumentException('Query and fragment must be a string');
477
        }
478
479
        return preg_replace_callback(
480
            '/(?:[^'.self::$charUnreserved.self::$charSubDelims.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
481
            [$this, 'rawurlencodeMatchZero'],
482
            $str
483
        );
484
    }
485
486
    private function rawurlencodeMatchZero(array $match): string
487
    {
488
        return rawurlencode($match[0]);
489
    }
490
}
491