Completed
Push — v3 ( 4ecdc9...abfb2c )
by Mihail
06:27
created

Uri::withPort()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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