Uri   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 422
Duplicated Lines 0 %

Importance

Changes 14
Bugs 0 Features 0
Metric Value
eloc 90
c 14
b 0
f 0
dl 0
loc 422
rs 9.52
wmc 36

22 Methods

Rating   Name   Duplication   Size   Complexity  
A withFragment() 0 3 1
A getScheme() 0 3 1
A withHost() 0 9 2
A getUserInfo() 0 3 1
A with() 0 10 2
A withPath() 0 3 1
A getPath() 0 3 1
A getAuthority() 0 6 1
A withPort() 0 11 3
A getFragment() 0 3 1
A withScheme() 0 10 3
A withQuery() 0 3 1
A __toString() 0 8 1
A getQuery() 0 3 1
A normalize() 0 8 1
A getHost() 0 3 1
A withUserInfo() 0 12 2
A encode() 0 10 1
A __construct() 0 16 3
A getPort() 0 7 2
A constructString() 0 11 2
A getNormalizedUriPath() 0 11 4
1
<?php
2
3
namespace Riimu\Kit\UrlParser;
4
5
use Psr\Http\Message\UriInterface;
6
7
/**
8
 * Immutable value object that represents a RFC3986 compliant URI.
9
 * @author Riikka Kalliomäki <[email protected]>
10
 * @copyright Copyright (c) 2015-2017 Riikka Kalliomäki
11
 * @license http://opensource.org/licenses/mit-license.php MIT License
12
 */
13
class Uri implements UriInterface
14
{
15
    use ExtendedUriTrait;
16
17
    /** @var string The scheme component of the URI */
18
    private $scheme = '';
19
20
    /** @var string The user information component of the URI */
21
    private $userInfo = '';
22
23
    /** @var string The host component of the URI */
24
    private $host = '';
25
26
    /** @var int|null The port component of the URI or null for none */
27
    private $port = null;
28
29
    /** @var string The path component of the URI */
30
    private $path = '';
31
32
    /** @var string The query component of the URI */
33
    private $query = '';
34
35
    /** @var string The fragment component of the URI */
36
    private $fragment = '';
37
38
    /**
39
     * Creates a new instance of Uri.
40
     * @param string $uri The URI provided as a string or empty string for none
41
     * @param int $mode The parser mode used to parse the provided URI
42
     * @throws \InvalidArgumentException If the provided URI is invalid
43
     */
44
    public function __construct($uri = '', $mode = UriParser::MODE_RFC3986)
45
    {
46
        $uri = (string) $uri;
47
48
        if ($uri !== '') {
49
            $parser = new UriParser();
50
            $parser->setMode($mode);
51
            $parsed = $parser->parse($uri);
52
53
            if (!$parsed instanceof self) {
54
                throw new \InvalidArgumentException("Invalid URI '$uri'");
55
            }
56
57
            $properties = get_object_vars($parsed);
58
            array_walk($properties, function ($value, $name) {
59
                $this->$name = $value;
60
            });
61
        }
62
    }
63
64
    /**
65
     * Returns the scheme component of the URI.
66
     *
67
     * Note that the returned value will always be normalized to lowercase,
68
     * as per RFC 3986 Section 3.1. If no scheme has been provided, an empty
69
     * string will be returned instead.
70
     *
71
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
72
     * @return string The URI scheme or an empty string if no scheme has been provided
73
     */
74
    public function getScheme()
75
    {
76
        return $this->scheme;
77
    }
78
79
    /**
80
     * Returns the authority component of the URI.
81
     *
82
     * If no authority information has been provided, an empty string will be
83
     * returned instead. Note that the host component in the authority component
84
     * will always be normalized to lowercase as per RFC 3986 Section 3.2.2.
85
     *
86
     * Also note that even if a port has been provided, but it is the standard port
87
     * for the current scheme, the port will not be included in the returned value.
88
     *
89
     * The format of the returned value is `[user-info@]host[:port]`
90
     *
91
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
92
     * @return string The URI authority or an empty string if no authority information has been provided
93
     */
94
    public function getAuthority()
95
    {
96
        return $this->constructString([
97
            '%s%s@' => $this->getUserInfo(),
98
            '%s%s' => $this->getHost(),
99
            '%s:%s' => $this->getPort(),
100
        ]);
101
    }
102
103
    /**
104
     * Returns the user information component of the URI.
105
     *
106
     * The user information component contains the username and password in the
107
     * URI separated by a colon. If no username has been provided, an empty
108
     * string will be returned instead. If no password has been provided, the returned
109
     * value will only contain the username without the delimiting colon.
110
     *
111
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.1
112
     * @return string The URI user information or an empty string if no username has been provided
113
     */
114
    public function getUserInfo()
115
    {
116
        return $this->userInfo;
117
    }
118
119
    /**
120
     * Returns the host component of the URI.
121
     *
122
     * Note that the returned value will always be normalized to lowercase,
123
     * as per RFC 3986 Section 3.2.2. If no host has been provided, an empty
124
     * string will be returned instead.
125
     *
126
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
127
     * @return string The URI host or an empty string if no host has been provided
128
     */
129
    public function getHost()
130
    {
131
        return $this->host;
132
    }
133
134
    /**
135
     * Returns the port component of the URI.
136
     *
137
     * If no port has been provided, this method will return a null instead.
138
     * Note that this method will also return a null, if the provided port is
139
     * the standard port for the current scheme.
140
     *
141
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.3
142
     * @return int|null The URI port or null if no port has been provided
143
     */
144
    public function getPort()
145
    {
146
        if ($this->port === $this->getStandardPort()) {
147
            return null;
148
        }
149
150
        return $this->port;
151
    }
152
153
    /**
154
     * Returns the path component of the URI.
155
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
156
     * @return string The URI path or an empty string if no path has been provided
157
     */
158
    public function getPath()
159
    {
160
        return $this->path;
161
    }
162
163
    /**
164
     * Returns the query string of the URI.
165
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
166
     * @return string The URI query string or an empty string if no query has been provided
167
     */
168
    public function getQuery()
169
    {
170
        return $this->query;
171
    }
172
173
    /**
174
     * Returns the fragment component of the URI.
175
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
176
     * @return string The URI fragment or an empty string if no fragment has been provided
177
     */
178
    public function getFragment()
179
    {
180
        return $this->fragment;
181
    }
182
183
    /**
184
     * Returns a URI instance with the specified scheme.
185
     *
186
     * This method allows all different kinds of schemes. Note, however, that
187
     * the different components are only validated based on the generic URI
188
     * syntax. No additional validation is performed based on the scheme. An
189
     * empty string can be used to remove the scheme. Note that the provided
190
     * scheme will be normalized to lowercase.
191
     *
192
     * @param string $scheme The scheme to use with the new instance
193
     * @return static A new instance with the specified scheme
194
     * @throws \InvalidArgumentException If the scheme is invalid
195
     */
196
    public function withScheme($scheme)
197
    {
198
        $scheme = strtolower($scheme);
199
        $pattern = new UriPattern();
200
201
        if (strlen($scheme) === 0 || $pattern->matchScheme($scheme)) {
202
            return $this->with('scheme', $scheme);
203
        }
204
205
        throw new \InvalidArgumentException("Invalid scheme '$scheme'");
206
    }
207
208
    /**
209
     * Returns a URI instance with the specified user information.
210
     *
211
     * Note that the password is optional, but unless an username is provided,
212
     * the password will be ignored. Note that this method assumes that neither
213
     * the username nor the password contains encoded characters. Thus, all
214
     * encoded characters will be double encoded, if present. An empty username
215
     * can be used to remove the user information.
216
     *
217
     * @param string $user The username to use for the authority component
218
     * @param string|null $password The password associated with the user
219
     * @return static A new instance with the specified user information
220
     */
221
    public function withUserInfo($user, $password = null)
222
    {
223
        $username = rawurlencode($user);
224
225
        if (strlen($username) > 0) {
226
            return $this->with('userInfo', $this->constructString([
227
                '%s%s' => $username,
228
                '%s:%s' => rawurlencode((string) $password),
229
            ]));
230
        }
231
232
        return $this->with('userInfo', '');
233
    }
234
235
    /**
236
     * Returns a URI instance with the specified host.
237
     *
238
     * An empty host can be used to remove the host. Note that since host names
239
     * are treated in a case insensitive manner, the host will be normalized
240
     * to lowercase. This method does not support international domain names and
241
     * hosts with non ascii characters are considered invalid.
242
     *
243
     * @param string $host The hostname to use with the new instance
244
     * @return static A new instance with the specified host
245
     * @throws \InvalidArgumentException If the hostname is invalid
246
     */
247
    public function withHost($host)
248
    {
249
        $pattern = new UriPattern();
250
251
        if ($pattern->matchHost($host)) {
252
            return $this->with('host', $this->normalize(strtolower($host)));
253
        }
254
255
        throw new \InvalidArgumentException("Invalid host '$host'");
256
    }
257
258
    /**
259
     * Returns a URI instance with the specified port.
260
     *
261
     * A null value can be used to remove the port number. Note that if an
262
     * invalid port number is provided (a number less than 0 or more than
263
     * 65535), an exception will be thrown.
264
     *
265
     * @param int|null $port The port to use with the new instance
266
     * @return static A new instance with the specified port
267
     * @throws \InvalidArgumentException If the port is invalid
268
     */
269
    public function withPort($port)
270
    {
271
        if ($port !== null) {
272
            $port = (int) $port;
273
274
            if (max(0, min(65535, $port)) !== $port) {
275
                throw new \InvalidArgumentException("Invalid port number '$port'");
276
            }
277
        }
278
279
        return $this->with('port', $port);
280
    }
281
282
    /**
283
     * Returns a URI instance with the specified path.
284
     *
285
     * The provided path may or may not begin with a forward slash. The path
286
     * will be automatically normalized with the appropriate number of slashes
287
     * once the string is generated from the Uri instance. An empty string can
288
     * be used to remove the path. The path may also contain percent encoded
289
     * characters as these characters will not be double encoded.
290
     *
291
     * @param string $path The path to use with the new instance
292
     * @return static A new instance with the specified path
293
     */
294
    public function withPath($path)
295
    {
296
        return $this->with('path', $this->encode($path, '@/'));
297
    }
298
299
    /**
300
     * Returns a URI instance with the specified query string.
301
     *
302
     * An empty string can be used to remove the query string. The provided
303
     * value may contain both encoded and unencoded characters. Encoded
304
     * characters will not be double encoded.
305
     *
306
     * @param string $query The query string to use with the new instance
307
     * @return static A new instance with the specified query string
308
     */
309
    public function withQuery($query)
310
    {
311
        return $this->with('query', $this->encode($query, ':@/?'));
312
    }
313
314
    /**
315
     * Returns a URI instance with the specified URI fragment.
316
     *
317
     * An empty string can be used to remove the fragment. The provided value
318
     * may contain both encoded and unencoded characters. Encoded characters
319
     * will not be double encoded.
320
     *
321
     * @param string $fragment The fragment to use with the new instance
322
     * @return static A new instance with the specified fragment
323
     */
324
    public function withFragment($fragment)
325
    {
326
        return $this->with('fragment', $this->encode($fragment, ':@/?'));
327
    }
328
329
    /**
330
     * Returns an Uri instance with the given value.
331
     * @param string $variable Name of the variable to change
332
     * @param mixed $value New value for the variable
333
     * @return static A new or the same instance depending on if the value changed
334
     */
335
    private function with($variable, $value)
336
    {
337
        if ($value === $this->$variable) {
338
            return $this;
339
        }
340
341
        $uri = clone $this;
342
        $uri->$variable = $value;
343
344
        return $uri;
345
    }
346
347
    /**
348
     * Percent encodes the value without double encoding.
349
     * @param string $string The value to encode
350
     * @param string $extra Additional allowed characters in the value
351
     * @return string The encoded string
352
     */
353
    private function encode($string, $extra = '')
354
    {
355
        $pattern = sprintf(
356
            '/[^0-9a-zA-Z%s]|%%(?![0-9A-F]{2})/',
357
            preg_quote("%-._~!$&'()*+,;=" . $extra, '/')
358
        );
359
360
        return preg_replace_callback($pattern, function ($match) {
361
            return sprintf('%%%02X', ord($match[0]));
362
        }, $this->normalize($string));
363
    }
364
365
    /**
366
     * Normalizes the percent encoded characters to upper case.
367
     * @param string $string The string to normalize
368
     * @return string String with percent encodings normalized to upper case
369
     */
370
    private function normalize($string)
371
    {
372
        return preg_replace_callback(
373
            '/%(?=.?[a-f])[0-9a-fA-F]{2}/',
374
            function ($match) {
375
                return strtoupper($match[0]);
376
            },
377
            (string) $string
378
        );
379
    }
380
381
    /**
382
     * Returns the string representation of the URI.
383
     *
384
     * The resulting URI will be composed of the provided components. All
385
     * components that have not been provided will be omitted from the generated
386
     * URI. The provided path will be normalized based on whether the authority
387
     * is included in the URI or not.
388
     *
389
     * @return string The string representation of the URI
390
     */
391
    public function __toString()
392
    {
393
        return $this->constructString([
394
            '%s%s:' => $this->getScheme(),
395
            '%s//%s' => $this->getAuthority(),
396
            '%s%s' => $this->getNormalizedUriPath(),
397
            '%s?%s' => $this->getQuery(),
398
            '%s#%s' => $this->getFragment(),
399
        ]);
400
    }
401
402
    /**
403
     * Constructs the string from the non empty parts with specific formats.
404
     * @param array $components Associative array of formats and values
405
     * @return string The constructed string
406
     */
407
    private function constructString(array $components)
408
    {
409
        $formats = array_keys($components);
410
        $values = array_values($components);
411
        $keys = array_keys(array_filter($values, function ($value) {
412
            return $value !== null && $value !== '';
413
        }));
414
415
        return array_reduce($keys, function ($string, $key) use ($formats, $values) {
416
            return sprintf($formats[$key], $string, $values[$key]);
417
        }, '');
418
    }
419
420
    /**
421
     * Returns the path normalized for the string representation.
422
     * @return string The normalized path for the string representation
423
     */
424
    private function getNormalizedUriPath()
425
    {
426
        $path = $this->getPath();
427
428
        if ($this->getAuthority() === '') {
429
            return preg_replace('#^/+#', '/', $path);
430
        } elseif ($path === '' || $path[0] === '/') {
431
            return $path;
432
        }
433
434
        return '/' . $path;
435
    }
436
}
437