Completed
Push — master ( c22a1a...c65aed )
by
unknown
02:00
created

Uri::withPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
rs 9.9
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2.0185
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
final class Uri implements UriInterface
18
{
19
    private static $schemes = ['http' => 80, 'https' => 443];
20
21
    private static $charUnreserved = 'a-zA-Z0-9_\-\.~';
22
23
    private static $charSubDelims = '!\$&\'\(\)\*\+,;=';
24
25
    /** @var string Uri scheme. */
26
    private $scheme = '';
27
28
    /** @var string Uri user info. */
29
    private $userInfo = '';
30
31
    /** @var string Uri host. */
32
    private $host = '';
33
34
    /** @var int|null Uri port. */
35
    private $port;
36
37
    /** @var string Uri path. */
38
    private $path = '';
39
40
    /** @var string Uri query string. */
41
    private $query = '';
42
43
    /** @var string Uri fragment. */
44
    private $fragment = '';
45
46 145
    public function __construct(string $uri = '')
47
    {
48 145
        if ('' !== $uri) {
49 110
            if (false === $parts = parse_url($uri)) {
50 4
                throw new \InvalidArgumentException("Unable to parse URI: $uri");
51
            }
52
53 106
            $this->applyParts($parts);
54
        }
55 141
    }
56
57 65
    public function __toString(): string
58
    {
59 65
        return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
60
    }
61
62 23
    public function getScheme(): string
63
    {
64 23
        return $this->scheme;
65
    }
66
67 71
    public function getAuthority(): string
68
    {
69 71
        if ('' === $this->host) {
70 29
            return '';
71
        }
72
73 44
        $authority = $this->host;
74 44
        if ('' !== $this->userInfo) {
75 7
            $authority = $this->userInfo.'@'.$authority;
76
        }
77
78 44
        if (null !== $this->port) {
79 7
            $authority .= ':'.$this->port;
80
        }
81
82 44
        return $authority;
83
    }
84
85 7
    public function getUserInfo(): string
86
    {
87 7
        return $this->userInfo;
88
    }
89
90 86
    public function getHost(): string
91
    {
92 86
        return $this->host;
93
    }
94
95 41
    public function getPort(): ?int
96
    {
97 41
        return $this->port;
98
    }
99
100 23
    public function getPath(): string
101
    {
102 23
        return $this->path;
103
    }
104
105 19
    public function getQuery(): string
106
    {
107 19
        return $this->query;
108
    }
109
110 17
    public function getFragment(): string
111
    {
112 17
        return $this->fragment;
113
    }
114
115 27
    public function withScheme($scheme): self
116
    {
117 27
        if ($this->scheme === $scheme = $this->filterScheme($scheme)) {
118
            return $this;
119
        }
120
121 22
        $new = clone $this;
122 22
        $new->scheme = $scheme;
123 22
        $new->port = $new->filterPort($new->port);
124
125 22
        return $new;
126
    }
127
128 5
    public function withUserInfo($user, $password = null): self
129
    {
130 5
        $info = $user;
131 5
        if (null !== $password && '' !== $password) {
132 5
            $info .= ':'.$password;
133
        }
134
135 5
        if ($this->userInfo === $info) {
136
            return $this;
137
        }
138
139 5
        $new = clone $this;
140 5
        $new->userInfo = $info;
141
142 5
        return $new;
143
    }
144
145 7
    public function withHost($host): self
146
    {
147 7
        if ($this->host === $host = $this->filterHost($host)) {
148
            return $this;
149
        }
150
151 6
        $new = clone $this;
152 6
        $new->host = $host;
153
154 6
        return $new;
155
    }
156
157 9
    public function withPort($port): self
158
    {
159 9
        if ($this->port === $port = $this->filterPort($port)) {
160 1
            return $this;
161
        }
162
163 6
        $new = clone $this;
164 6
        $new->port = $port;
165
166 6
        return $new;
167
    }
168
169 9
    public function withPath($path): self
170
    {
171 9
        if ($this->path === $path = $this->filterPath($path)) {
172
            return $this;
173
        }
174
175 8
        $new = clone $this;
176 8
        $new->path = $path;
177
178 8
        return $new;
179
    }
180
181 6
    public function withQuery($query): self
182
    {
183 6
        if ($this->query === $query = $this->filterQueryAndFragment($query)) {
184
            return $this;
185
        }
186
187 5
        $new = clone $this;
188 5
        $new->query = $query;
189
190 5
        return $new;
191
    }
192
193 6
    public function withFragment($fragment): self
194
    {
195 6
        if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
196
            return $this;
197
        }
198
199 5
        $new = clone $this;
200 5
        $new->fragment = $fragment;
201
202 5
        return $new;
203
    }
204
205
    /**
206
     * Apply parse_url parts to a URI.
207
     *
208
     * @param array $parts Array of parse_url parts to apply
209
     */
210 106
    private function applyParts(array $parts): void
211
    {
212 106
        $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : '';
213 106
        $this->userInfo = isset($parts['user']) ? $parts['user'] : '';
214 106
        $this->host = isset($parts['host']) ? $this->filterHost($parts['host']) : '';
215 106
        $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
216 106
        $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
217 106
        $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
218 106
        $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
219 106
        if (isset($parts['pass'])) {
220 5
            $this->userInfo .= ':'.$parts['pass'];
221
        }
222 106
    }
223
224
    /**
225
     * Create a URI string from its various parts.
226
     */
227 65
    private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
228
    {
229 65
        $uri = '';
230 65
        if ('' !== $scheme) {
231 40
            $uri .= $scheme.':';
232
        }
233
234 65
        if ('' !== $authority) {
235 40
            $uri .= '//'.$authority;
236
        }
237
238 65
        if ('' !== $path) {
239 53
            if ('/' !== $path[0]) {
240 7
                if ('' !== $authority) {
241
                    // If the path is rootless and an authority is present, the path MUST be prefixed by "/"
242 7
                    $path = '/'.$path;
243
                }
244 46
            } elseif (isset($path[1]) && '/' === $path[1]) {
245 1
                if ('' === $authority) {
246
                    // If the path is starting with more than one "/" and no authority is present, the
247
                    // starting slashes MUST be reduced to one.
248 1
                    $path = '/'.ltrim($path, '/');
249
                }
250
            }
251
252 53
            $uri .= $path;
253
        }
254
255 65
        if ('' !== $query) {
256 35
            $uri .= '?'.$query;
257
        }
258
259 65
        if ('' !== $fragment) {
260 16
            $uri .= '#'.$fragment;
261
        }
262
263 65
        return $uri;
264
    }
265
266
    /**
267
     * Is a given port non-standard for the current scheme?
268
     */
269 12
    private static function isNonStandardPort(string $scheme, int $port): bool
270
    {
271 12
        return !isset(self::$schemes[$scheme]) || $port !== self::$schemes[$scheme];
272
    }
273
274 74
    private function filterScheme($scheme): string
275
    {
276 74
        if (!is_string($scheme)) {
277 5
            throw new \InvalidArgumentException('Scheme must be a string');
278
        }
279
280 69
        return strtolower($scheme);
281
    }
282
283 57
    private function filterHost($host): string
284
    {
285 57
        if (!is_string($host)) {
286 1
            throw new \InvalidArgumentException('Host must be a string');
287
        }
288
289 56
        return strtolower($host);
290
    }
291
292 32
    private function filterPort($port): ?int
293
    {
294 32
        if (null === $port) {
295 21
            return null;
296
        }
297
298 14
        $port = (int) $port;
299 14
        if (1 > $port || 0xffff < $port) {
300 2
            throw new \InvalidArgumentException(sprintf('Invalid port: %d. Must be between 1 and 65535', $port));
301
        }
302
303 12
        return self::isNonStandardPort($this->scheme, $port) ? $port : null;
304
    }
305
306 102
    private function filterPath($path): string
307
    {
308 102
        if (!is_string($path)) {
309 1
            throw new \InvalidArgumentException('Path must be a string');
310
        }
311
312 101
        return preg_replace_callback('/(?:[^'.self::$charUnreserved.self::$charSubDelims.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $path);
313
    }
314
315 40
    private function filterQueryAndFragment($str): string
316
    {
317 40
        if (!is_string($str)) {
318 2
            throw new \InvalidArgumentException('Query and fragment must be a string');
319
        }
320
321 38
        return preg_replace_callback('/(?:[^'.self::$charUnreserved.self::$charSubDelims.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $str);
322
    }
323
324 6
    private function rawurlencodeMatchZero(array $match): string
325
    {
326 6
        return rawurlencode($match[0]);
327
    }
328
}
329