Completed
Push — master ( c448e0...8d38f7 )
by Tobias
19:28 queued 17:43
created

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