Completed
Push — master ( d2aa89...bbfa5f )
by Mihail
34:12 queued 24:23
created

Uri   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 212
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 97
c 5
b 0
f 0
dl 0
loc 212
ccs 91
cts 91
cp 1
rs 8.48
wmc 49

23 Methods

Rating   Name   Duplication   Size   Complexity  
A getQuery() 0 3 1
A getAuthority() 0 7 2
A jsonSerialize() 0 11 1
A getScheme() 0 3 1
A withHost() 0 5 1
A getHost() 0 3 1
A withScheme() 0 9 3
A withQuery() 0 10 2
A getFragment() 0 3 1
A reduceSlashes() 0 6 3
A parse() 0 11 4
A __construct() 0 3 2
A fixPath() 0 13 4
A __toString() 0 8 5
A withPort() 0 12 4
A isStandardPort() 0 3 1
A getHostWithPort() 0 6 3
A getUserInfo() 0 6 2
A getPath() 0 3 1
A withPath() 0 5 1
A withFragment() 0 5 1
A getPort() 0 6 3
A withUserInfo() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like Uri often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Uri, and based on these observations, apply Extract Interface, too.

1
<?php
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
21
class Uri implements UriInterface, JsonSerializable
22
{
23
    const STANDARD_PORTS = [80, 443, 21, 23, 70, 110, 119, 143, 389];
24
25
    private string $scheme = '';
26
    private string $host = '';
27
    private ?int $port = 80;
28
    private string $path = '';
29
    private string $user = '';
30
    private string $pass = '';
31
    private string $fragment = '';
32
    private string $query = '';
33
34
    public function __construct(string $uri)
35
    {
36
        $uri && $this->parse($uri);
37
    }
38
39
    public function __toString()
40
    {
41
        return \sprintf('%s%s%s%s%s',
42
            $this->scheme ? ($this->getScheme() . '://') : '',
43
            $this->getAuthority() ?: $this->getHostWithPort(),
44
            $this->getPath(),
45
            \strlen($this->query) ? ('?' . $this->query) : '',
46 174
            \strlen($this->fragment) ? ('#' . $this->fragment) : ''
47
        );
48 174
    }
49 173
50
    public function getScheme(): string
51 26
    {
52
        return \strtolower($this->scheme);
53 26
    }
54 26
55 26
    public function getAuthority(): string
56 26
    {
57 26
        $userInfo = $this->getUserInfo();
58 26
        if (0 === \strlen($userInfo)) {
59
            return '';
60
        }
61
        return $userInfo . '@' . $this->getHostWithPort();
62 26
    }
63
64 26
    public function getUserInfo(): string
65
    {
66
        if (0 === \strlen($this->user)) {
67 30
            return '';
68
        }
69 30
        return \trim($this->user . ':' . $this->pass, ':');
70
    }
71 30
72 26
    public function getHost(): string
73
    {
74
        return \strtolower($this->host);
75 6
    }
76
77
    public function getPort(): ?int
78 34
    {
79
        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...
80 34
            return null;
81 30
        }
82
        return $this->port;
83
    }
84 8
85
    public function getPath(): string
86
    {
87 135
        return $this->reduceSlashes($this->path);
88
    }
89 135
90
    public function getQuery(): string
91
    {
92 9
        return $this->query;
93
    }
94 9
95 1
    public function getFragment(): string
96
    {
97
        return $this->fragment;
98 8
    }
99
100
    public function withScheme($scheme): UriInterface
101 46
    {
102
        if (null !== $scheme && false === \is_string($scheme)) {
103 46
            throw new InvalidArgumentException('Invalid URI scheme', 400);
104
        }
105
106 9
        $instance         = clone $this;
107
        $instance->scheme = (string)$scheme;
108 9
        return $instance;
109
    }
110
111 8
    public function withUserInfo($user, $password = null): UriInterface
112
    {
113 8
        $instance       = clone $this;
114
        $instance->user = (string)$user;
115
        $instance->pass = (string)$password;
116 8
117
        // If the path is rootless and an authority is present,
118 8
        // the path MUST be prefixed with "/"
119 4
        if ('/' !== ($instance->path[0] ?? '')) {
120
            $instance->path = '/' . $instance->path;
121
        }
122 4
        return $instance;
123 4
    }
124
125 4
    public function withHost($host): UriInterface
126
    {
127
        $instance       = clone $this;
128 4
        $instance->host = (string)$host;
129
        return $instance;
130 4
    }
131 4
132 4
    public function withPort($port): UriInterface
133
    {
134
        $instance = clone $this;
135
        if (null === $port) {
136 4
            $instance->port = null;
137 2
            return $instance;
138
        }
139
        if (false === \is_int($port) || $port < 1) {
140 4
            throw new InvalidArgumentException('Invalid port');
141
        }
142
        $instance->port = $port;
143 7
        return $instance;
144
    }
145 7
146 7
    public function withPath($path): UriInterface
147
    {
148 7
        $instance       = clone $this;
149
        $instance->path = $this->fixPath((string)$path);
150
        return $instance;
151 6
    }
152
153 6
    public function withQuery($query): UriInterface
154
    {
155 6
        try {
156 2
            $query = \rawurldecode($query);
157
        } catch (Throwable) {
158 2
            throw new InvalidArgumentException('The provided query string is invalid');
159
        }
160
        $instance        = clone $this;
161 4
        $instance->query = (string)$query;
162 1
        return $instance;
163
    }
164
165 3
    public function withFragment($fragment): UriInterface
166
    {
167 3
        $instance           = clone $this;
168
        $instance->fragment = \str_replace(['#', '%23'], '', $fragment);
169
        return $instance;
170 4
    }
171
172 4
    private function parse(string $uri)
173 4
    {
174
        if (false === $parts = \parse_url($uri)) {
175 4
            throw new InvalidArgumentException('Please provide a valid URI', HttpStatus::BAD_REQUEST);
176
        }
177
        foreach ($parts as $k => $v) {
178 3
            $this->$k = $v;
179
        }
180
        $this->path = $this->fixPath($parts['path'] ?? '');
181 3
        if ($this->isStandardPort()) {
182 1
            $this->port = null;
183 1
        }
184
    }
185
186 2
    private function fixPath(string $path): string
187 2
    {
188
        if (empty($path)) {
189 2
            return $path;
190
        }
191
        // Percent encode the path
192 3
        $path = \explode('/', $path);
193
        foreach ($path as $k => $part) {
194 3
            $path[$k] = \str_contains($part, '%') ? $part : \rawurlencode($part);
195 3
        }
196
        // TODO remove the entry script from the path?
197 3
        $path = \str_replace('/index.php', '', \join('/', $path));
198
        return $path;
199
    }
200 138
201
    private function reduceSlashes(string $path): string
202 138
    {
203 3
        if ('/' === ($path[0] ?? '') && 0 === \strlen($this->user)) {
204
            return \preg_replace('/\/+/', '/', $path);
205
        }
206 137
        return $path;
207 137
    }
208
209
    private function getHostWithPort(): string
210 137
    {
211
        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...
212 137
            return $this->host . ($this->isStandardPort() ? '' : ':' . $this->port);
213 99
        }
214
        return $this->host;
215 137
    }
216
217 138
    private function isStandardPort(): bool
218
    {
219 138
        return \in_array($this->port, static::STANDARD_PORTS);
220 79
    }
221
222
    public function jsonSerialize()
223
    {
224 65
        return \array_filter([
225 65
            'scheme' => $this->getScheme(),
226 65
            'host' => $this->getHost(),
227
            'port' => $this->getPort(),
228
            'path' => $this->getPath(),
229
            'user' => $this->user,
230 65
            'pass' => $this->pass,
231
            'fragment' => $this->fragment,
232 65
            'query' => $this->query,
233
        ]);
234
    }
235
}
236