Passed
Pull Request — master (#17)
by Mihail
15:10
created

Uri::fixPath()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
c 3
b 0
f 0
nc 4
nop 1
dl 0
loc 13
ccs 7
cts 7
cp 1
crap 4
rs 10
1
<?php declare(strict_types=1);
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http;
14
15
use InvalidArgumentException;
16
use JsonSerializable;
17
use Koded\Http\Interfaces\HttpStatus;
18
use Psr\Http\Message\UriInterface;
19
use Throwable;
20
use function array_filter;
21
use function explode;
22
use function implode;
23
use function mb_strlen;
24
use function parse_url;
25
use function preg_replace;
26
use function rawurldecode;
27
use function rawurlencode;
28
use function sprintf;
29
use function str_contains;
30
use function str_replace;
31
use function strtolower;
32
use function trim;
33
34
class Uri implements UriInterface, JsonSerializable
35
{
36
    public static array $standardPorts = [
37
        80  => true,
38
        443 => true,
39
        21  => true,
40
        23  => true,
41
        70  => true,
42
        110 => true,
43
        119 => true,
44
        143 => true,
45
        389 => true,
46 174
    ];
47
48 174
    private string $scheme   = '';
49 173
    private string $host     = '';
50
    private ?int $port       = 80;
51 26
    private string $path     = '';
52
    private string $user     = '';
53 26
    private string $pass     = '';
54 26
    private string $fragment = '';
55 26
    private string $query    = '';
56 26
57 26
    public function __construct(string $uri)
58 26
    {
59
        $uri && $this->parse($uri);
60
    }
61
62 26
    public function __toString(): string
63
    {
64 26
        return sprintf('%s%s%s%s%s',
65
            $this->scheme ? ($this->getScheme() . '://') : '',
66
            $this->getAuthority() ?: $this->getHostWithPort(),
67 30
            $this->getPath(),
68
            mb_strlen($this->query) ? ("?$this->query") : '',
69 30
            mb_strlen($this->fragment) ? ("#$this->fragment") : ''
70
        );
71 30
    }
72 26
73
    public function getScheme(): string
74
    {
75 6
        return strtolower($this->scheme);
76
    }
77
78 34
    public function getAuthority(): string
79
    {
80 34
        return ($userInfo = $this->getUserInfo())
81 30
            ? $userInfo . '@' . $this->getHostWithPort()
82
            : '';
83
    }
84 8
85
    public function getUserInfo(): string
86
    {
87 135
        if (empty($this->user)) {
88
            return '';
89 135
        }
90
        return trim(rawurlencode($this->user) . ':' . rawurlencode($this->pass), ':');
91
    }
92 9
93
    public function getHost(): string
94 9
    {
95 1
        return mb_strtolower($this->host);
96
    }
97
98 8
    public function getPort(): ?int
99
    {
100
        if (!$this->scheme && !$this->port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->port of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
101 46
            return null;
102
        }
103 46
        return $this->port;
104
    }
105
106 9
    public function getPath(): string
107
    {
108 9
        $path = $this->path;
109
        // If the path is rootless and an authority is present,
110
        // the path MUST be prefixed with "/"
111 8
        if ($this->user && '/' !== ($path[0] ?? '')) {
112
            return "/$path";
113 8
        }
114
        // If the path is starting with more than one "/" and no authority is
115
        // present, the starting slashes MUST be reduced to one
116 8
        if (!$this->user && '/' === ($path[0] ?? '') && '/' === ($path[1] ?? '')) {
117
            $path = preg_replace('/\/+/', '/', $path);
118 8
        }
119 4
        // Percent encode the path
120
        $path = explode('/', $path);
121
        foreach ($path as $k => $part) {
122 4
            $path[$k] = str_contains($part, '%') ? $part : rawurlencode($part);
123 4
        }
124
        // TODO remove the entry script from the path?
125 4
        return str_replace('/index.php', '', implode('/', $path));
126
    }
127
128 4
    public function getQuery(): string
129
    {
130 4
        return $this->query;
131 4
    }
132 4
133
    public function getFragment(): string
134
    {
135
        return $this->fragment;
136 4
    }
137 2
138
    public function withScheme(string $scheme): UriInterface
139
    {
140 4
        $instance         = clone $this;
141
        $instance->scheme = $scheme;
142
        return $instance;
143 7
    }
144
145 7
    public function withUserInfo(string $user, ?string $password = null): UriInterface
146 7
    {
147
        $instance       = clone $this;
148 7
        $instance->user = rawurldecode($user);
149
        $instance->pass = rawurldecode((string)$password);
150
        return $instance;
151 6
    }
152
153 6
    public function withHost(string $host): UriInterface
154
    {
155 6
        $instance       = clone $this;
156 2
        $instance->host = $host;
157
        return $instance;
158 2
    }
159
160
    public function withPort(?int $port): UriInterface
161 4
    {
162 1
        $instance = clone $this;
163
        if (null === $port) {
164
            $instance->port = null;
165 3
            return $instance;
166
        }
167 3
        if ($port < 1) {
168
            throw new InvalidArgumentException('Invalid port', HttpStatus::BAD_REQUEST);
169
        }
170 4
        $instance->port = $port;
171
        return $instance;
172 4
    }
173 4
174
    public function withPath(string $path): UriInterface
175 4
    {
176
        $instance       = clone $this;
177
        $instance->path = $path;
178 3
        return $instance;
179
    }
180
181 3
    public function withQuery(string $query): UriInterface
182 1
    {
183 1
        try {
184
            $query = rawurldecode($query);
185
        } catch (Throwable) {
186 2
            throw new InvalidArgumentException(
187 2
                'The provided query string is invalid',
188
                HttpStatus::BAD_REQUEST);
189 2
        }
190
        $instance        = clone $this;
191
        $instance->query = $query;
192 3
        return $instance;
193
    }
194 3
195 3
    public function withFragment(string $fragment): UriInterface
196
    {
197 3
        $instance           = clone $this;
198
        $instance->fragment = str_replace(['#', '%23'], '', $fragment);
199
        return $instance;
200 138
    }
201
202 138
    private function parse(string $uri)
203 3
    {
204
        if (false === $parts = parse_url($uri)) {
205
            throw new InvalidArgumentException(
206 137
                'Please provide a valid URI',
207 137
                HttpStatus::BAD_REQUEST);
208
        }
209
        $this->port = (int) ($parts['port'] ?? 443);
210 137
        unset($parts['port']);
211
        foreach ($parts as $k => $v) {
212 137
            $this->$k = trim($v);
213 99
        }
214
        if ($this->isStandardPort()) {
215 137
            $this->port = null;
216
        }
217 138
    }
218
219 138
    private function getHostWithPort(): string
220 79
    {
221
        if ($this->port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->port of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
222
            return $this->host . ($this->isStandardPort() ? '' : ":$this->port");
223
        }
224 65
        return $this->host;
225 65
    }
226 65
227
    private function isStandardPort(): bool
228
    {
229
        return isset(static::$standardPorts[$this->port]);
230 65
    }
231
232 65
    public function jsonSerialize(): mixed
233
    {
234
        return array_filter([
235 46
            'scheme'   => $this->getScheme(),
236
            'host'     => $this->getHost(),
237 46
            'port'     => $this->getPort(),
238 19
            'path'     => $this->getPath(),
239
            'user'     => $this->user,
240
            'pass'     => $this->pass,
241 30
            'fragment' => $this->fragment,
242
            'query'    => $this->query,
243
        ]);
244 29
    }
245
}
246