Completed
Push — master ( 588b5e...a331b6 )
by Matthew
04:08
created

Uri::withPort()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 9.2
cc 4
eloc 6
nc 3
nop 1
crap 4
1
<?php
2
namespace Fyuze\Http\Message;
3
4
use InvalidArgumentException;
5
use Psr\Http\Message\UriInterface;
6
use RuntimeException;
7
8
class Uri implements UriInterface
9
{
10
    /**
11
     * @var string
12
     */
13
    protected $uri;
14
15
    /**
16
     * @var string
17
     */
18
    protected $scheme = '';
19
20
    /**
21
     * @var string
22
     */
23
    protected $authority;
24
25
    /**
26
     * @var string
27
     */
28
    protected $user;
29
30
    /**
31
     * @var string
32
     */
33
    protected $pass;
34
35
    /**
36
     * @var string
37
     */
38
    protected $userInfo;
39
40
    /**
41
     * @var string
42
     */
43
    protected $host;
44
45
    /**
46
     * @var int
47
     */
48
    protected $port;
49
50
    /**
51
     * @var string
52
     */
53
    protected $path;
54
55
    /**
56
     * @var string
57
     */
58
    protected $query = '';
59
60
    /**
61
     * @var string
62
     */
63
    protected $fragment;
64
65
    /**
66
     * @var array
67
     */
68
    protected $schemes = [
69
        'http' => 80,
70
        'https' => 443,
71
    ];
72
73
    /**
74
     * @param $uri
75
     */
76 53
    public function __construct($uri = '')
77
    {
78 53
        if ($uri !== '') {
79 36
            $this->format($uri);
80 35
        }
81
82 53
        $this->uri = $uri;
83 53
    }
84
85
    /**
86
     * {@inheritdoc}
87
     *
88
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
89
     * @return string The URI scheme.
90
     */
91 12
    public function getScheme()
92
    {
93 12
        return $this->scheme;
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     *
99
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
100
     * @return string The URI authority, in "[user-info@]host[:port]" format.
101
     */
102 9
    public function getAuthority()
103
    {
104 9
        $userInfo = $this->getUserInfo();
105
106 9
        return rtrim(($userInfo ? $userInfo . '@' : '')
107 9
            . $this->host
108 9
            . (!$this->isStandardPort($this->scheme, $this->port) ? ':' . $this->port : ''),
109 9
            ':');
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     *
115
     * @return string The URI user information, in "username[:password]" format.
116
     */
117 12
    public function getUserInfo()
118
    {
119 12
        return $this->userInfo;
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     *
125
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
126
     * @return string The URI host.
127
     */
128 6
    public function getHost()
129
    {
130 6
        return $this->host;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     *
136
     * @return null|int The URI port.
137
     */
138 6
    public function getPort()
139
    {
140 6
        return $this->isStandardPort($this->scheme, $this->port)
141 6
            ? null : $this->port;
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     *
147
     * @see https://tools.ietf.org/html/rfc3986#section-2
148
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
149
     * @return string The URI path.
150
     */
151 23
    public function getPath()
152
    {
153 23
        return $this->path;
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     *
159
     * @see https://tools.ietf.org/html/rfc3986#section-2
160
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
161
     * @return string The URI query string.
162
     */
163 14
    public function getQuery()
164
    {
165 14
        return $this->query;
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     *
171
     * @see https://tools.ietf.org/html/rfc3986#section-2
172
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
173
     * @return string The URI fragment.
174
     */
175 15
    public function getFragment()
176
    {
177 15
        return $this->fragment;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     *
183
     * @param string $scheme The scheme to use with the new instance.
184
     * @return self A new instance with the specified scheme.
185
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
186
     */
187 6
    public function withScheme($scheme)
188
    {
189 6
        $scheme = str_replace('://', '', strtolower((string)$scheme));
190
191 6
        if (!empty($scheme) && !array_key_exists($scheme, $this->schemes)) {
192 1
            throw new InvalidArgumentException('Invalid scheme provided.');
193
        }
194
195 5
        return $this->_clone('scheme', $scheme);
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     *
201
     * @param string $user The user name to use for authority.
202
     * @param null|string $password The password associated with $user.
203
     * @return self A new instance with the specified user information.
204
     */
205 4
    public function withUserInfo($user, $password = null)
206
    {
207 4
        $userInfo = implode(':', [$user, $password]);
208
209 4
        return $this->_clone('userInfo', rtrim($userInfo, ':'));
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     *
215
     * @param string $host
216
     * @return Uri
217
     */
218 3
    public function withHost($host)
219
    {
220 3
        return $this->_clone('host', $host);
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     *
226
     * @param null|int $port The port to use with the new instance; a null value
227
     *     removes the port information.
228
     * @return self A new instance with the specified port.
229
     * @throws InvalidArgumentException for invalid ports.
230
     */
231 5
    public function withPort($port)
232
    {
233 5
        if ($port === null) {
234 1
            return $this->_clone('port', null);
235
        }
236
237 5
        if ($port < 1 || $port > 65535) {
238 1
            throw new InvalidArgumentException('Invalid port specified');
239
        }
240
241 4
        return $this->_clone('port', (int)$port);
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     *
247
     * @param string $path The path to use with the new instance.
248
     * @return self A new instance with the specified path.
249
     * @throws \InvalidArgumentException for invalid paths.
250
     */
251 7
    public function withPath($path)
252
    {
253 7
        if (!is_string($path) || strpos($path, '#') !== false || strpos($path, '?') !== false) {
254 2
            throw new \InvalidArgumentException('Path must be a string and cannot contain a fragment or query string');
255
        }
256
257 5
        if (!empty($path) && '/' !== substr($path, 0, 1)) {
258 4
            $path = '/' . $path;
259 4
        }
260
261 5
        return $this->_clone('path', $this->filterPath($path));
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     *
267
     * @param string $query The query string to use with the new instance.
268
     * @return self A new instance with the specified query string.
269
     * @throws \InvalidArgumentException for invalid query strings.
270
     */
271 5
    public function withQuery($query)
272
    {
273 5
        return $this->_clone('query', $this->filter($query));
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     *
279
     * @param string $fragment The fragment to use with the new instance.
280
     * @return self A new instance with the specified fragment.
281
     */
282 7
    public function withFragment($fragment)
283
    {
284 7
        if (strpos($fragment, '#') === 0) {
285 2
            $fragment = substr($fragment, 1);
286 2
        }
287
288 7
        return $this->_clone('fragment', $this->filter($fragment));
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     *
294
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
295
     * @return string
296
     */
297 8
    public function __toString()
298
    {
299 8
        $scheme = $this->getScheme();
300 8
        $authority = $this->getAuthority();
301 8
        $path = $this->getPath();
302 8
        $query = $this->getQuery();
303 8
        $fragment = $this->getFragment();
304
305 8
        return ($scheme ? $scheme . '://' : '')
306 8
        . ($authority ? '' . $authority : '')
307 8
        . $path
308 8
        . ($query ? '?' . $query : '')
309 8
        . ($fragment ? '#' . $fragment : '');
310
    }
311
312
    /**
313
     * @param $uri
314
     */
315 36
    protected function format($uri)
316
    {
317 36
        $parsed = parse_url($uri);
318
319 36
        if (false === $parsed) {
320 1
            throw new RuntimeException('Seriously malformed url');
321
        }
322
323 35
        foreach (parse_url($uri) as $key => $val) {
1 ignored issue
show
Bug introduced by
The expression parse_url($uri) of type array<string,string>|false is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
324 35
            $this->$key = $val;
325 35
        }
326
327 35
        $this->userInfo = $this->user . ($this->pass ? ":$this->pass" : null);
328 35
    }
329
330
331
    /**
332
     * Filters the query string or fragment of a URI.
333
     *
334
     * @param string $query The raw uri query string.
335
     * @return string The percent-encoded query string.
336
     */
337 7
    protected function filter($query)
338
    {
339 7
        $decoded = rawurldecode($query);
340
341 7
        if ($decoded === $query) {
342
            // Query string or fragment is already decoded, encode
343 6
            return str_replace(['%3D', '%26'], ['=', '&'], rawurlencode($query));
344
        }
345
346 1
        return $query;
347
    }
348
349
    /**
350
     * Filter Uri path.
351
     *
352
     * This method percent-encodes all reserved
353
     * characters in the provided path string. This method
354
     * will NOT double-encode characters that are already
355
     * percent-encoded.
356
     *
357
     * @param  string $path The raw uri path.
358
     * @return string       The RFC 3986 percent-encoded uri path.
359
     * @link   http://www.faqs.org/rfcs/rfc3986.html
360
     */
361 5
    protected function filterPath($path)
362
    {
363 5
        $decoded = rawurldecode($path);
364
365 5
        if ($decoded === $path) {
366
            // Url is already decoded, encode
367 4
            return str_replace('%2F', '/', rawurlencode($path));
368
        }
369
370 1
        return $path;
371
    }
372
373
    /**
374
     * @param $scheme
375
     * @param $port
376
     * @return bool
377
     */
378 14
    protected function isStandardPort($scheme, $port)
379
    {
380 14
        if (!$scheme && !$port) {
381 8
            return true;
382
        }
383
384 7
        if (array_key_exists($scheme, $this->schemes)
385 7
            && $port === $this->schemes[$scheme]
386 7
        ) {
387 1
            return true;
388
        }
389
390 6
        return false;
391
    }
392
393
    /**
394
     * @param $key
395
     * @param $value
396
     * @return static
397
     */
398 14
    protected function _clone($key, $value)
399
    {
400 14
        if ($this->$key === $value) {
401 1
            return clone($this);
402
        }
403
404 14
        $instance = clone $this;
405 14
        $instance->$key = $value;
406 14
        return $instance;
407
    }
408
}
409