Test Failed
Push — master ( 0b6467...e6142c )
by Alexpts
16:38
created

Uri::__construct()   B

Complexity

Conditions 8
Paths 34

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 13
c 1
b 0
f 0
nc 34
nop 1
dl 0
loc 17
ccs 14
cts 14
cp 1
crap 8
rs 8.4444
1
<?php
2
3
namespace PTS\Psr7;
4
5
use InvalidArgumentException;
6
use Psr\Http\Message\UriInterface;
7
use function is_string;
8
use function parse_url;
9
use function preg_replace_callback;
10
use function rawurlencode;
11
use function sprintf;
12
13
class Uri implements UriInterface
14
{
15
    use LowercaseTrait;
16
17
    protected const SCHEMES = ['http' => 80, 'https' => 443];
18
    protected const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
19
    protected const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
20
21
    protected string $scheme = '';
22
    protected string $userInfo = '';
23
    protected string $host = '';
24
25
    protected ?int $port = null;
26
    protected string $path = '';
27
    protected string $query = '';
28
    protected string $fragment = '';
29
30 90
    public function __construct(string $uri = '')
31
    {
32 90
        if ('' !== $uri) {
33 71
            $parts = parse_url($uri);
34 71
            if ($parts === false) {
35 3
                throw new InvalidArgumentException("Unable to parse URI: $uri");
36
            }
37
38 68
            $this->withScheme($parts['scheme'] ?? '');
39 68
            $this->userInfo = $parts['user'] ?? '';
40 68
            $this->withHost($parts['host'] ?? '');
41 68
            $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
42 68
            $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
43 68
            $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
44 68
            $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
45 68
            if (isset($parts['pass'])) {
46 2
                $this->userInfo .= ':' . $parts['pass'];
47
            }
48
        }
49 87
    }
50
51 40
    public function __toString(): string
52
    {
53 40
        return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
54
    }
55
56 7
    public function getScheme(): string
57
    {
58 7
        return $this->scheme;
59
    }
60
61 45
    public function getAuthority(): string
62
    {
63 45
        if ('' === $this->host) {
64 28
            return '';
65
        }
66
67 17
        $authority = $this->host;
68 17
        if ('' !== $this->userInfo) {
69 4
            $authority = $this->userInfo . '@' . $authority;
70
        }
71
72 17
        if (null !== $this->port) {
73 4
            $authority .= ':' . $this->port;
74
        }
75
76 17
        return $authority;
77
    }
78
79 6
    public function getUserInfo(): string
80
    {
81 6
        return $this->userInfo;
82
    }
83
84 45
    public function getHost(): string
85
    {
86 45
        return $this->host;
87
    }
88
89 22
    public function getPort(): ?int
90
    {
91 22
        return $this->port;
92
    }
93
94 20
    public function getPath(): string
95
    {
96 20
        return $this->path;
97
    }
98
99 27
    public function getQuery(): string
100
    {
101 27
        return $this->query;
102
    }
103
104 12
    public function getFragment(): string
105
    {
106 12
        return $this->fragment;
107
    }
108
109 70
    public function withScheme($scheme): self
110
    {
111 70
        $this->scheme = self::lowercase($scheme);
112 70
        $this->port = $this->port ? $this->filterPort($this->port) : null;
113
114 70
        return $this;
115
    }
116
117 3
    public function withUserInfo($user, $password = null): self
118
    {
119 3
        $info = $user;
120 3
        if (null !== $password && '' !== $password) {
121 3
            $info .= ':' . $password;
122
        }
123
124 3
        $this->userInfo = $info;
125
126 3
        return $this;
127
    }
128
129 71
    public function withHost($host): self
130
    {
131 71
        $this->host = self::lowercase($host);
132 71
        return $this;
133
    }
134
135 7
    public function withPort($port): self
136
    {
137 7
        $this->port = $port === null ? null : $this->filterPort($port);
138 5
        return $this;
139
    }
140
141 7
    public function withPath($path): self
142
    {
143 7
        $path = $this->filterPath($path);
144 6
        if ($this->path !== $path) {
145 6
            $this->path = $path;
146
        }
147
148 6
        return $this;
149
    }
150
151 4
    public function withQuery($query): self
152
    {
153 4
        $query = $this->filterQueryAndFragment($query);
154 3
        if ($this->query !== $query) {
155 3
            $this->query = $query;
156
        }
157
158 3
        return $this;
159
    }
160
161 4
    public function withFragment($fragment): self
162
    {
163 4
        $fragment = $this->filterQueryAndFragment($fragment);
164 3
        if ($this->fragment !== $fragment) {
165 3
            $this->fragment = $fragment;
166
        }
167
168 3
        return $this;
169
    }
170
171 40
    private static function createUriString(
172
        string $scheme,
173 40
        string $authority,
174 40
        string $path,
175 13
        string $query,
176
        string $fragment
177
    ): string {
178 40
        $uri = '';
179 14
        if ('' !== $scheme) {
180
            $uri .= $scheme . ':';
181
        }
182 40
183 26
        if ('' !== $authority) {
184 7
            $uri .= '//' . $authority;
185
        }
186 7
187
        if ('' !== $path) {
188 19
            if ('/' !== $path[0]) {
189 1
                if ('' !== $authority) {
190
                    // If the path is rootless and an authority is present, the path MUST be prefixed by "/"
191
                    $path = '/' . $path;
192 1
                }
193
            } elseif (isset($path[1]) && '/' === $path[1]) {
194
                if ('' === $authority) {
195
                    // If the path is starting with more than one "/" and no authority is present, the
196 26
                    // starting slashes MUST be reduced to one.
197
                    $path = '/' . \ltrim($path, '/');
198
                }
199 40
            }
200 14
201
            $uri .= $path;
202
        }
203 40
204 13
        if ('' !== $query) {
205
            $uri .= '?' . $query;
206
        }
207 40
208
        if ('' !== $fragment) {
209
            $uri .= '#' . $fragment;
210 10
        }
211
212 10
        return $uri;
213
    }
214
215 12
    private static function isNonStandardPort(string $scheme, int $port): bool
216
    {
217 12
        return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
218 2
    }
219
220
    protected function filterPort(int $port): ?int
221 10
    {
222
        if ($port < 1 || $port > 65535) {
223
            throw new InvalidArgumentException(sprintf('Invalid port: %d. Must be between 0 and 65535', $port));
224 61
        }
225
226 61
        return self::isNonStandardPort($this->scheme, $port) ? $port : null;
227 1
    }
228
229
    private function filterPath($path): string
230 60
    {
231
        if (!is_string($path)) {
232
            throw new InvalidArgumentException('Path must be a string');
233 26
        }
234
235 26
        return preg_replace_callback('/(?:[^' .
236 2
            self::CHAR_UNRESERVED .
237
            self::CHAR_SUB_DELIMS .
238
            '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
239 24
    }
240
241
    private function filterQueryAndFragment($str): string
242 6
    {
243
        if (!is_string($str)) {
244 6
            throw new InvalidArgumentException('Query and fragment must be a string');
245
        }
246
247
        return preg_replace_callback('/(?:[^' .
248
            self::CHAR_UNRESERVED .
249
            self::CHAR_SUB_DELIMS .
250
            '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
251
    }
252
253
    private static function rawUrlEncodeMatchZero(array $match): string
0 ignored issues
show
Unused Code introduced by
The method rawUrlEncodeMatchZero() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
254
    {
255
        return rawurlencode($match[0]);
256
    }
257
}