Completed
Push — master ( a59df7...fcae40 )
by Derek
02:07
created

Uri::withPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 1
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
crap 2
1
<?php
2
namespace Subreality\Dilmun\Anshar\Http;
3
4
use Psr\Http\Message\UriInterface;
5
use Subreality\Dilmun\Anshar\Utils\ArrayHelper;
6
use Subreality\Dilmun\Anshar\Utils\StringHelper;
7
8
class Uri implements UriInterface
9
{
10
    use SchemePortsTrait;
11
12
    protected $uri_parts = array(
13
        "scheme"    => "",
14
        "hier_part" => "",
15
        "authority" => "",
16
        "user_info" => "",
17
        "host"      => "",
18
        "port"      => null,
19
        "path"      => "",
20
        "query"     => "",
21
        "fragment"  => "",
22
    );
23
24
    /**
25
     * Uri constructor.  Accepts a string representing a URI and parses the string into the URI's component parts.
26
     *
27
     * @throws \InvalidArgumentException    Throws an \InvalidArgumentException when its parameter is not a string
28
     * @param string $uri
29
     */
30 30
    public function __construct($uri)
31
    {
32 30
        if (!is_string($uri)) {
33 6
            throw new \InvalidArgumentException("New Uri objects must be constructed with a string URI");
34
        }
35
36 24
        $this->explodeUri($uri);
37 24
    }
38
39
    /**
40
     * Retrieve the parsed components of the URI string.
41
     *
42
     * If the class was provided an invalid URI string, URI components will be empty strings, except port, which will
43
     * be null
44
     *
45
     * @return mixed[]
46
     */
47 11
    public function getParsedUri()
48
    {
49 11
        return $this->uri_parts;
50
    }
51
52
    /**
53
     * Retrieve the scheme component of the URI.
54
     *
55
     * If no scheme is present, this method MUST return an empty string.
56
     *
57
     * The value returned MUST be normalized to lowercase, per RFC 3986
58
     * Section 3.1.
59
     *
60
     * The trailing ":" character is not part of the scheme and MUST NOT be
61
     * added.
62
     *
63
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
64
     * @return string The URI scheme.
65
     */
66 3
    public function getScheme()
67
    {
68 3
        return strtolower($this->uri_parts["scheme"]);
69
    }
70
71
    /**
72
     * Retrieve the authority component of the URI.
73
     *
74
     * If no authority information is present, this method MUST return an empty
75
     * string.
76
     *
77
     * The authority syntax of the URI is:
78
     *
79
     * <pre>
80
     * [user-info@]host[:port]
81
     * </pre>
82
     *
83
     * If the port component is not set or is the standard port for the current
84
     * scheme, it SHOULD NOT be included.
85
     *
86
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
87
     * @return string The URI authority, in "[user-info@]host[:port]" format.
88
     */
89 4
    public function getAuthority()
90
    {
91 4
        $normalized_authority = $this->uri_parts["host"];
92
93 4
        if (!empty($this->uri_parts["user_info"])) {
94 4
            $normalized_authority = $this->uri_parts["user_info"] . "@" . $normalized_authority;
95 4
        }
96
97 4
        $normalized_port = $this->normalizePort();
98
99 4
        if (!is_null($normalized_port)) {
100 2
            $normalized_authority = $normalized_authority . ":" . $normalized_port;
101 2
        }
102
103 4
        return $normalized_authority;
104
    }
105
106
    /**
107
     * Retrieve the user information component of the URI.
108
     *
109
     * If no user information is present, this method MUST return an empty
110
     * string.
111
     *
112
     * If a user is present in the URI, this will return that value;
113
     * additionally, if the password is also present, it will be appended to the
114
     * user value, with a colon (":") separating the values.
115
     *
116
     * The trailing "@" character is not part of the user information and MUST
117
     * NOT be added.
118
     *
119
     * @return string The URI user information, in "username[:password]" format.
120
     */
121 2
    public function getUserInfo()
122
    {
123 2
        return $this->uri_parts["user_info"];
124
    }
125
126
    /**
127
     * Retrieve the host component of the URI.
128
     *
129
     * If no host is present, this method MUST return an empty string.
130
     *
131
     * The value returned MUST be normalized to lowercase, per RFC 3986
132
     * Section 3.2.2.
133
     *
134
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
135
     * @return string The URI host.
136
     */
137 3
    public function getHost()
138
    {
139 3
        return strtolower($this->uri_parts["host"]);
140
    }
141
142
    /**
143
     * Retrieve the port component of the URI.
144
     *
145
     * If a port is present, and it is non-standard for the current scheme,
146
     * this method MUST return it as an integer. If the port is the standard port
147
     * used with the current scheme, this method SHOULD return null.
148
     *
149
     * If no port is present, and no scheme is present, this method MUST return
150
     * a null value.
151
     *
152
     * If no port is present, but a scheme is present, this method MAY return
153
     * the standard port for that scheme, but SHOULD return null.
154
     *
155
     * @return null|int The URI port.
156
     */
157 4
    public function getPort()
158
    {
159 4
        $normalized_port = $this->normalizePort();
160
161 4
        return $normalized_port;
162
    }
163
164
    /**
165
     * Retrieve the path component of the URI.
166
     *
167
     * The path can either be empty or absolute (starting with a slash) or
168
     * rootless (not starting with a slash). Implementations MUST support all
169
     * three syntaxes.
170
     *
171
     * Normally, the empty path "" and absolute path "/" are considered equal as
172
     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
173
     * do this normalization because in contexts with a trimmed base path, e.g.
174
     * the front controller, this difference becomes significant. It's the task
175
     * of the user to handle both "" and "/".
176
     *
177
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
178
     * any characters. To determine what characters to encode, please refer to
179
     * RFC 3986, Sections 2 and 3.3.
180
     *
181
     * As an example, if the value should include a slash ("/") not intended as
182
     * delimiter between path segments, that value MUST be passed in encoded
183
     * form (e.g., "%2F") to the instance.
184
     *
185
     * @see https://tools.ietf.org/html/rfc3986#section-2
186
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
187
     * @return string The URI path.
188
     */
189 3
    public function getPath()
190
    {
191
        $unencoded      = array(
192 3
            "/",
193 3
            ":",
194 3
            "@",
195 3
            "!",
196 3
            "$",
197 3
            "&",
198 3
            "'",
199 3
            "(",
200 3
            ")",
201 3
            "*",
202 3
            "+",
203 3
            ",",
204 3
            ";",
205 3
            "=",
206 3
        );
207 3
        $string_helper  = new StringHelper($this->uri_parts["path"]);
208 3
        $encoded_string = $string_helper->affectChunks("rawurlencode", ...$unencoded);
209
210 3
        return $encoded_string;
211
    }
212
213
    /**
214
     * Retrieve the query string of the URI.
215
     *
216
     * If no query string is present, this method MUST return an empty string.
217
     *
218
     * The leading "?" character is not part of the query and MUST NOT be
219
     * added.
220
     *
221
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
222
     * any characters. To determine what characters to encode, please refer to
223
     * RFC 3986, Sections 2 and 3.4.
224
     *
225
     * As an example, if a value in a key/value pair of the query string should
226
     * include an ampersand ("&") not intended as a delimiter between values,
227
     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
228
     *
229
     * @see https://tools.ietf.org/html/rfc3986#section-2
230
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
231
     * @return string The URI query string.
232
     */
233
    public function getQuery()
234
    {
235
        return $this->uri_parts["query"];
236
    }
237
238
    /**
239
     * Retrieve the fragment component of the URI.
240
     *
241
     * If no fragment is present, this method MUST return an empty string.
242
     *
243
     * The leading "#" character is not part of the fragment and MUST NOT be
244
     * added.
245
     *
246
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
247
     * any characters. To determine what characters to encode, please refer to
248
     * RFC 3986, Sections 2 and 3.5.
249
     *
250
     * @see https://tools.ietf.org/html/rfc3986#section-2
251
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
252
     * @return string The URI fragment.
253
     */
254
    public function getFragment()
255
    {
256
        return $this->uri_parts["fragment"];
257
    }
258
259
    /**
260
     * Return an instance with the specified scheme.
261
     *
262
     * This method MUST retain the state of the current instance, and return
263
     * an instance that contains the specified scheme.
264
     *
265
     * Implementations MUST support the schemes "http" and "https" case
266
     * insensitively, and MAY accommodate other schemes if required.
267
     *
268
     * An empty scheme is equivalent to removing the scheme.
269
     *
270
     * @param string $scheme The scheme to use with the new instance.
271
     * @return static A new instance with the specified scheme.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
272
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
273
     */
274
    public function withScheme($scheme)
275
    {
276
        // TODO: Implement withScheme() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
277
    }
278
279
    /**
280
     * Return an instance with the specified user information.
281
     *
282
     * This method MUST retain the state of the current instance, and return
283
     * an instance that contains the specified user information.
284
     *
285
     * Password is optional, but the user information MUST include the
286
     * user; an empty string for the user is equivalent to removing user
287
     * information.
288
     *
289
     * @param string $user The user name to use for authority.
290
     * @param null|string $password The password associated with $user.
291
     * @return static A new instance with the specified user information.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
292
     */
293
    public function withUserInfo($user, $password = null)
294
    {
295
        // TODO: Implement withUserInfo() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
296
    }
297
298
    /**
299
     * Return an instance with the specified host.
300
     *
301
     * This method MUST retain the state of the current instance, and return
302
     * an instance that contains the specified host.
303
     *
304
     * An empty host value is equivalent to removing the host.
305
     *
306
     * @param string $host The hostname to use with the new instance.
307
     * @return static A new instance with the specified host.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
308
     * @throws \InvalidArgumentException for invalid hostnames.
309
     */
310
    public function withHost($host)
311
    {
312
        // TODO: Implement withHost() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
313
    }
314
315
    /**
316
     * Return an instance with the specified port.
317
     *
318
     * This method MUST retain the state of the current instance, and return
319
     * an instance that contains the specified port.
320
     *
321
     * Implementations MUST raise an exception for ports outside the
322
     * established TCP and UDP port ranges.
323
     *
324
     * A null value provided for the port is equivalent to removing the port
325
     * information.
326
     *
327
     * @param null|int $port The port to use with the new instance; a null value
328
     *     removes the port information.
329
     * @return static A new instance with the specified port.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
330
     * @throws \InvalidArgumentException for invalid ports.
331
     */
332
    public function withPort($port)
333
    {
334
        // TODO: Implement withPort() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
335
    }
336
337
    /**
338
     * Return an instance with the specified path.
339
     *
340
     * This method MUST retain the state of the current instance, and return
341
     * an instance that contains the specified path.
342
     *
343
     * The path can either be empty or absolute (starting with a slash) or
344
     * rootless (not starting with a slash). Implementations MUST support all
345
     * three syntaxes.
346
     *
347
     * If the path is intended to be domain-relative rather than path relative then
348
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
349
     * are assumed to be relative to some base path known to the application or
350
     * consumer.
351
     *
352
     * Users can provide both encoded and decoded path characters.
353
     * Implementations ensure the correct encoding as outlined in getPath().
354
     *
355
     * @param string $path The path to use with the new instance.
356
     * @return static A new instance with the specified path.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
357
     * @throws \InvalidArgumentException for invalid paths.
358
     */
359
    public function withPath($path)
360
    {
361
        // TODO: Implement withPath() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
362
    }
363
364
    /**
365
     * Return an instance with the specified query string.
366
     *
367
     * This method MUST retain the state of the current instance, and return
368
     * an instance that contains the specified query string.
369
     *
370
     * Users can provide both encoded and decoded query characters.
371
     * Implementations ensure the correct encoding as outlined in getQuery().
372
     *
373
     * An empty query string value is equivalent to removing the query string.
374
     *
375
     * @param string $query The query string to use with the new instance.
376
     * @return static A new instance with the specified query string.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
377
     * @throws \InvalidArgumentException for invalid query strings.
378
     */
379
    public function withQuery($query)
380
    {
381
        // TODO: Implement withQuery() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
382
    }
383
384
    /**
385
     * Return an instance with the specified URI fragment.
386
     *
387
     * This method MUST retain the state of the current instance, and return
388
     * an instance that contains the specified URI fragment.
389
     *
390
     * Users can provide both encoded and decoded fragment characters.
391
     * Implementations ensure the correct encoding as outlined in getFragment().
392
     *
393
     * An empty fragment value is equivalent to removing the fragment.
394
     *
395
     * @param string $fragment The fragment to use with the new instance.
396
     * @return static A new instance with the specified fragment.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
397
     */
398
    public function withFragment($fragment)
399
    {
400
        // TODO: Implement withFragment() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
401
    }
402
403
    /**
404
     * Return the string representation as a URI reference.
405
     *
406
     * Depending on which components of the URI are present, the resulting
407
     * string is either a full URI or relative reference according to RFC 3986,
408
     * Section 4.1. The method concatenates the various components of the URI,
409
     * using the appropriate delimiters:
410
     *
411
     * - If a scheme is present, it MUST be suffixed by ":".
412
     * - If an authority is present, it MUST be prefixed by "//".
413
     * - The path can be concatenated without delimiters. But there are two
414
     *   cases where the path has to be adjusted to make the URI reference
415
     *   valid as PHP does not allow to throw an exception in __toString():
416
     *     - If the path is rootless and an authority is present, the path MUST
417
     *       be prefixed by "/".
418
     *     - If the path is starting with more than one "/" and no authority is
419
     *       present, the starting slashes MUST be reduced to one.
420
     * - If a query is present, it MUST be prefixed by "?".
421
     * - If a fragment is present, it MUST be prefixed by "#".
422
     *
423
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
424
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
425
     */
426
    public function __toString()
427
    {
428
        // TODO: Implement __toString() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
429
    }
430
431
    /**
432
     * Splits a string URI into its component parts, returning true if the URI string matches a valid URI's syntax
433
     * and false if the URI string does not
434
     *
435
     * @param string $uri   The URI string to be decomposed
436
     * @return bool         Returns true if the URI string matches a valid URI's syntax
437
     *                      Returns false otherwise
438
     */
439 24
    private function explodeUri($uri)
440
    {
441 24
        $reg_start        = '/^';
442 24
        $scheme_part      = '(?P<scheme>.[^:]+)';
443 24
        $scheme_separator = ':';
444 24
        $hier_part        = '(?<hier_part>.[^\?#]+)';
445 24
        $query_part       = '(?:\?(?P<query>.[^#]+))?';
446 24
        $fragment_part    = '(?:#(?P<fragment>.+))?';
447 24
        $reg_end          = '/';
448
449 24
        $uri_syntax = $reg_start . $scheme_part . $scheme_separator . $hier_part . $query_part . $fragment_part .
450 24
            $reg_end;
451
452 24
        $uri_valid = preg_match($uri_syntax, $uri, $parts);
453
454 24
        $this->uri_parts = array_merge($this->uri_parts, $parts); //overwriting default values with matches
455
456 24
        $this->explodeHierParts($this->uri_parts["hier_part"]);
457
458 24
        $this->sanitizeUriPartsArray();
459
460 24
        return (bool) $uri_valid;
461
    }
462
463
    /**
464
     * Splits URI hierarchy data into authority and path data.
465
     *
466
     * @param string $hier_part     The hierarchy part of a URI to be decomposed
467
     * @return void
468
     */
469 24
    private function explodeHierParts($hier_part)
470
    {
471 24
        $authority_parts = array();
472
473 24
        $reg_start      = '/^';
474 24
        $authority_part = '(?:(?:\/\/)(?P<authority>.[^\/]+))?';
475 24
        $path_part      = '(?P<path>.+)?';
476 24
        $reg_end        = '/';
477
478 24
        $hier_part_syntax = $reg_start . $authority_part . $path_part . $reg_end;
479
480 24
        preg_match($hier_part_syntax, $hier_part, $hier_parts);
481
482 24
        if (isset($hier_parts["authority"])) {
483 20
            $authority_parts = $this->explodeAuthority($hier_parts["authority"]);
484 20
        }
485
486 24
        $hier_parts = array_merge($hier_parts, $authority_parts);
487
488 24
        $this->uri_parts = array_merge($this->uri_parts, $hier_parts);
489 24
    }
490
491
    /**
492
     * Splits URI authority data into user info, host, and port data, returning an array with named keys.
493
     *
494
     * For the host component, it will capture everything within brackets to support ipv6 or match all characters until
495
     * it finds a colon indicating the start of the port component.
496
     *
497
     * @param string $authority     The authority part of a URI to be decomposed
498
     * @return mixed[]              An array with named keys containing the component parts of the supplied
0 ignored issues
show
Documentation introduced by
Should the return type not be array<*,string|integer|null>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
499
     *                              authority
500
     */
501 20
    private function explodeAuthority($authority)
502
    {
503 20
        $reg_start      = '/^';
504 20
        $user_info_part = '(?:(?P<user_info>.+)@)?';
505 20
        $host_part      = '(?P<host>\[.+\]|.[^:]+)';
506 20
        $port_part      = '(?::(?P<port>[0-9]+))?';
507 20
        $reg_end        = '/';
508
509 20
        $authority_syntax = $reg_start . $user_info_part . $host_part . $port_part . $reg_end;
510
511 20
        preg_match($authority_syntax, $authority, $authority_parts);
512
513 20
        if (isset($authority_parts["port"])) {
514 8
            $authority_parts["port"] = (int) $authority_parts["port"];
515 8
        }
516
517 20
        return $authority_parts;
518
    }
519
520
    /**
521
     * Normalizes a port string based on whether the URI's port is standard for its scheme
522
     *
523
     * @return int|null     Returns null if the port is standard for the scheme
524
     *                      Returns the port prepended with a colon if the port is not standard for the scheme
525
     */
526 8
    private function normalizePort()
527
    {
528 8
        $scheme_port_array = new ArrayHelper($this->scheme_ports);
529
530 8
        $standard_port = $scheme_port_array->valueLookup($this->uri_parts["scheme"]);
531
532 8
        if ($this->uri_parts["port"] == $standard_port) {
533 5
            $normalized_port = null;
534 5
        } else {
535 3
            $normalized_port = $this->uri_parts["port"];
536
        }
537
538 8
        return $normalized_port;
539
    }
540
541
    /**
542
     * Sanitizes the URI component array by removing redundant key/value pairs
543
     *
544
     * @return void
545
     */
546 24
    private function sanitizeUriPartsArray()
547
    {
548 24
        $uri_part_array = new ArrayHelper($this->uri_parts);
549
550 24
        $this->uri_parts = $uri_part_array->removeNumericKeys();
551 24
    }
552
}
553