Completed
Push — master ( e78b35...c448e0 )
by Tobias
18:18 queued 08:25
created

Uri::applyParts()   B

Complexity

Conditions 8
Paths 128

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 8

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 8.2111
c 0
b 0
f 0
cc 8
nc 128
nop 1
crap 8
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 145
    public function __construct(string $uri = '')
48
    {
49 145
        if ('' !== $uri) {
50 125
            if (false === $parts = \parse_url($uri)) {
51 4
                throw new \InvalidArgumentException("Unable to parse URI: $uri");
52
            }
53
54 121
            $this->applyParts($parts);
55
        }
56 141
    }
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 86
    public function getHost(): string
92
    {
93 86
        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
            throw new \InvalidArgumentException('Scheme must be a string');
120
        }
121
122 7
        if ($this->scheme === $scheme = \strtolower($scheme)) {
123 7
            return $this;
124 7
        }
125
126 7
        $new = clone $this;
127
        $new->scheme = $scheme;
128
        $new->port = $new->filterPort($new->port);
129 5
130
        return $new;
131 5
    }
132 5
133 5
    public function withUserInfo($user, $password = null): self
134
    {
135
        $info = $user;
136 5
        if (null !== $password && '' !== $password) {
137
            $info .= ':' . $password;
138
        }
139
140 5
        if ($this->userInfo === $info) {
141 5
            return $this;
142
        }
143 5
144
        $new = clone $this;
145
        $new->userInfo = $info;
146 7
147
        return $new;
148 7
    }
149
150
    public function withHost($host): self
151
    {
152 6
        if (!\is_string($host)) {
153 6
            throw new \InvalidArgumentException('Host must be a string');
154
        }
155 6
156
        if ($this->host === $host = \strtolower($host)) {
157
            return $this;
158 9
        }
159
160 9
        $new = clone $this;
161 1
        $new->host = $host;
162
163
        return $new;
164 6
    }
165 6
166
    public function withPort($port): self
167 6
    {
168
        if ($this->port === $port = $this->filterPort($port)) {
169
            return $this;
170 9
        }
171
172 9
        $new = clone $this;
173
        $new->port = $port;
174
175
        return $new;
176 8
    }
177 8
178
    public function withPath($path): self
179 8
    {
180
        if ($this->path === $path = $this->filterPath($path)) {
181
            return $this;
182 6
        }
183
184 6
        $new = clone $this;
185
        $new->path = $path;
186
187
        return $new;
188 5
    }
189 5
190
    public function withQuery($query): self
191 5
    {
192
        if ($this->query === $query = $this->filterQueryAndFragment($query)) {
193
            return $this;
194 6
        }
195
196 6
        $new = clone $this;
197
        $new->query = $query;
198
199
        return $new;
200 5
    }
201 5
202
    public function withFragment($fragment): self
203 5
    {
204
        if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
205
            return $this;
206
        }
207
208
        $new = clone $this;
209
        $new->fragment = $fragment;
210
211 121
        return $new;
212
    }
213 121
214 121
    /**
215 121
     * Apply parse_url parts to a URI.
216 121
     *
217 121
     * @param array $parts Array of parse_url parts to apply
218 121
     */
219 121
    private function applyParts(array $parts): void
220 121
    {
221 5
        $this->scheme = isset($parts['scheme']) ? \strtolower($parts['scheme']) : '';
222
        $this->userInfo = $parts['user'] ?? '';
223 121
        $this->host = isset($parts['host']) ? \strtolower($parts['host']) : '';
224
        $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
225
        $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
226
        $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
227
        $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
228 65
        if (isset($parts['pass'])) {
229
            $this->userInfo .= ':' . $parts['pass'];
230 65
        }
231 65
    }
232 40
233
    /**
234
     * Create a URI string from its various parts.
235 65
     */
236 40
    private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
237
    {
238
        $uri = '';
239 65
        if ('' !== $scheme) {
240 53
            $uri .= $scheme . ':';
241 7
        }
242
243 7
        if ('' !== $authority) {
244
            $uri .= '//' . $authority;
245 46
        }
246 1
247
        if ('' !== $path) {
248
            if ('/' !== $path[0]) {
249 1
                if ('' !== $authority) {
250
                    // If the path is rootless and an authority is present, the path MUST be prefixed by "/"
251
                    $path = '/' . $path;
252
                }
253 53
            } elseif (isset($path[1]) && '/' === $path[1]) {
254
                if ('' === $authority) {
255
                    // If the path is starting with more than one "/" and no authority is present, the
256 65
                    // starting slashes MUST be reduced to one.
257 35
                    $path = '/' . \ltrim($path, '/');
258
                }
259
            }
260 65
261 16
            $uri .= $path;
262
        }
263
264 65
        if ('' !== $query) {
265
            $uri .= '?' . $query;
266
        }
267
268
        if ('' !== $fragment) {
269
            $uri .= '#' . $fragment;
270 12
        }
271
272 12
        return $uri;
273
    }
274
275 59
    /**
276
     * Is a given port non-standard for the current scheme?
277 59
     */
278 5
    private static function isNonStandardPort(string $scheme, int $port): bool
279
    {
280
        return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
281 54
    }
282
283
    private function filterPort($port): ?int
284 57
    {
285
        if (null === $port) {
286 57
            return null;
287 1
        }
288
289
        $port = (int)$port;
290 56
        if (1 > $port || 0xffff < $port) {
291
            throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 1 and 65535', $port));
292
        }
293 17
294
        return self::isNonStandardPort($this->scheme, $port) ? $port : null;
295 17
    }
296 6
297
    private function filterPath($path): string
298
    {
299 14
        if (!\is_string($path)) {
300 14
            throw new \InvalidArgumentException('Path must be a string');
301 2
        }
302
303
        return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
304 12
    }
305
306
    private function filterQueryAndFragment($str): string
307 117
    {
308
        if (!\is_string($str)) {
309 117
            throw new \InvalidArgumentException('Query and fragment must be a string');
310 1
        }
311
312
        return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
313 116
    }
314
315
    private static function rawurlencodeMatchZero(array $match): string
316 40
    {
317
        return \rawurlencode($match[0]);
318 40
    }
319
}
320