Completed
Push — master ( f73fec...6e38f9 )
by Derek
02:12
created

Uri   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 586
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 88.79%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
wmc 26
c 10
b 0
f 0
lcom 1
cbo 9
dl 0
loc 586
ccs 95
cts 107
cp 0.8879
rs 10

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A getScheme() 0 4 1
A getAuthority() 0 4 1
A getUserInfo() 0 4 1
A getHost() 0 4 1
A getPort() 0 6 1
A getParsedUri() 0 4 1
A getPath() 0 4 1
A getQuery() 0 4 1
A getFragment() 0 4 1
A withScheme() 0 4 1
A withAuthority() 0 14 1
A withUserInfo() 0 4 1
A withHost() 0 4 1
A withPort() 0 4 1
A withPath() 0 16 2
A withQuery() 0 4 1
A withFragment() 0 4 1
A __toString() 0 4 1
A toString() 0 16 1
A explodeUri() 0 23 1
A explodeHierParts() 0 20 1
A parseExplosions() 0 15 1
A sanitizeUriPartsArray() 0 6 1
1
<?php
2
namespace Subreality\Dilmun\Anshar\Http;
3
4
use Psr\Http\Message\UriInterface;
5
use Subreality\Dilmun\Anshar\Http\UriParts\Authority;
6
use Subreality\Dilmun\Anshar\Http\UriParts\Fragment;
7
use Subreality\Dilmun\Anshar\Http\UriParts\Host;
8
use Subreality\Dilmun\Anshar\Http\UriParts\Path;
9
use Subreality\Dilmun\Anshar\Http\UriParts\Port;
10
use Subreality\Dilmun\Anshar\Http\UriParts\Query;
11
use Subreality\Dilmun\Anshar\Http\UriParts\Scheme;
12
use Subreality\Dilmun\Anshar\Http\UriParts\UserInfo;
13
use Subreality\Dilmun\Anshar\Utils\ArrayHelper;
14
15
/**
16
 * Class Uri
17
 * @package Subreality\Dilmun\Anshar\Http
18
 */
19
class Uri implements UriInterface
20
{
21
    protected $uri_parts = array( //TODO: Probably don't need this anymore; refactor tests and remove
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...
22
        "scheme"    => "",
23
        "hier_part" => "",
24
        "authority" => "",
25
        "user_info" => "",
26
        "host"      => "",
27
        "port"      => null,
28
        "path"      => "",
29
        "query"     => "",
30
        "fragment"  => "",
31
    );
32
33
    /** @var  Scheme */
34
    protected $scheme;
35
    /** @var  Authority */
36
    protected $authority;
37
    /** @var  UserInfo */
38
    protected $user_info;
39
    /** @var  Host */
40
    protected $host;
41
    /** @var  Port */
42
    protected $port;
43
    /** @var  Path */
44
    protected $path;
45
    /** @var  Query */
46
    protected $query;
47
    /** @var  Fragment */
48
    protected $fragment;
49
50
    protected $sub_delims = array(
51
        "!",
52
        "$",
53
        "&",
54
        "'",
55
        "(",
56
        ")",
57
        "*",
58
        "+",
59
        ",",
60
        ";",
61
        "=",
62
    );
63
64
    protected $pchar_unencoded = array(
65
        ":",
66
        "@",
67
    );
68
69
    /**
70
     * Uri constructor.  Accepts a string representing a URI and parses the string into the URI's component parts.
71
     *
72
     * @throws \InvalidArgumentException    Throws an \InvalidArgumentException when its parameter is not a string
73
     * @param string $uri
74
     */
75 78
    public function __construct($uri)
76
    {
77 78
        if (!is_string($uri)) {
78 6
            throw new \InvalidArgumentException("New Uri objects must be constructed with a string URI");
79
        }
80
81 72
        $this->explodeUri($uri);
82 72
    }
83
84
    /**
85
     * Retrieve the parsed components of the URI string.
86
     *
87
     * If the class was provided an invalid or empty URI string, URI components will be empty strings, except port,
88
     * which will be null
89
     *
90
     * @return mixed[]
91
     */
92 43
    public function getParsedUri()
93
    {
94 43
        return $this->uri_parts;
95 10
    }
96
97
    /**
98
     * Retrieve the scheme component of the URI.
99
     *
100
     * If no scheme is present, this method MUST return an empty string.
101
     *
102
     * The value returned MUST be normalized to lowercase, per RFC 3986
103
     * Section 3.1.
104
     *
105
     * The trailing ":" character is not part of the scheme and MUST NOT be
106
     * added.
107
     *
108
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
109
     * @return string The URI scheme.
110
     */
111 3
    public function getScheme()
112
    {
113 3
        return (string) $this->scheme;
114
    }
115
116
    /**
117
     * Retrieve the authority component of the URI.
118
     *
119
     * If no authority information is present, this method MUST return an empty
120
     * string.
121
     *
122
     * The authority syntax of the URI is:
123
     *
124
     * <pre>
125
     * [user-info@]host[:port]
126
     * </pre>
127
     *
128
     * If the port component is not set or is the standard port for the current
129
     * scheme, it SHOULD NOT be included.
130
     *
131
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
132
     * @return string The URI authority, in "[user-info@]host[:port]" format.
133
     */
134 7
    public function getAuthority()
135
    {
136 7
        return $this->user_info->toUriString() . $this->host->toUriString() . $this->port->toUriString($this->scheme);
137
    }
138
139
    /**
140
     * Retrieve the user information component of the URI.
141
     *
142
     * If no user information is present, this method MUST return an empty
143
     * string.
144
     *
145
     * If a user is present in the URI, this will return that value;
146
     * additionally, if the password is also present, it will be appended to the
147
     * user value, with a colon (":") separating the values.
148
     *
149
     * The trailing "@" character is not part of the user information and MUST
150
     * NOT be added.
151
     *
152
     * @return string The URI user information, in "username[:password]" format.
153
     */
154 5
    public function getUserInfo()
155
    {
156 5
        return (string) $this->user_info;
157
    }
158
159
    /**
160
     * Retrieve the host component of the URI.
161
     *
162
     * If no host is present, this method MUST return an empty string.
163
     *
164
     * The value returned MUST be normalized to lowercase, per RFC 3986
165
     * Section 3.2.2.
166
     *
167
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
168
     * @return string The URI host.
169
     */
170 5
    public function getHost()
171
    {
172 5
        return (string) $this->host;
173
    }
174
175
    /**
176
     * Retrieve the port component of the URI.
177
     *
178
     * If a port is present, and it is non-standard for the current scheme,
179
     * this method MUST return it as an integer. If the port is the standard port
180
     * used with the current scheme, this method SHOULD return null.
181
     *
182
     * If no port is present, and no scheme is present, this method MUST return
183
     * a null value.
184
     *
185
     * If no port is present, but a scheme is present, this method MAY return
186
     * the standard port for that scheme, but SHOULD return null.
187
     *
188
     * @return null|int The URI port.
189
     */
190 7
    public function getPort()
191
    {
192 7
        $normalized_port = $this->port->normalizePortAgainstScheme($this->scheme);
193
194 7
        return $normalized_port;
195
    }
196
197
    /**
198
     * Retrieve the path component of the URI.
199
     *
200
     * The path can either be empty or absolute (starting with a slash) or
201
     * rootless (not starting with a slash). Implementations MUST support all
202
     * three syntaxes.
203
     *
204
     * Normally, the empty path "" and absolute path "/" are considered equal as
205
     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
206
     * do this normalization because in contexts with a trimmed base path, e.g.
207
     * the front controller, this difference becomes significant. It's the task
208
     * of the user to handle both "" and "/".
209
     *
210
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
211
     * any characters. To determine what characters to encode, please refer to
212
     * RFC 3986, Sections 2 and 3.3.
213
     *
214
     * As an example, if the value should include a slash ("/") not intended as
215
     * delimiter between path segments, that value MUST be passed in encoded
216
     * form (e.g., "%2F") to the instance.
217
     *
218
     * @see https://tools.ietf.org/html/rfc3986#section-2
219
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
220
     * @return string The URI path.
221
     */
222 8
    public function getPath()
223
    {
224 8
        return (string) $this->path;
225
    }
226
227
    /**
228
     * Retrieve the query string of the URI.
229
     *
230
     * If no query string is present, this method MUST return an empty string.
231
     *
232
     * The leading "?" character is not part of the query and MUST NOT be
233
     * added.
234
     *
235
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
236
     * any characters. To determine what characters to encode, please refer to
237
     * RFC 3986, Sections 2 and 3.4.
238
     *
239
     * As an example, if a value in a key/value pair of the query string should
240
     * include an ampersand ("&") not intended as a delimiter between values,
241
     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
242
     *
243
     * @see https://tools.ietf.org/html/rfc3986#section-2
244
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
245
     * @return string The URI query string.
246
     */
247 4
    public function getQuery()
248
    {
249 4
        return (string) $this->query;
250
    }
251
252
    /**
253
     * Retrieve the fragment component of the URI.
254
     *
255
     * If no fragment is present, this method MUST return an empty string.
256
     *
257
     * The leading "#" character is not part of the fragment and MUST NOT be
258
     * added.
259
     *
260
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
261
     * any characters. To determine what characters to encode, please refer to
262
     * RFC 3986, Sections 2 and 3.5.
263
     *
264
     * @see https://tools.ietf.org/html/rfc3986#section-2
265
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
266
     * @return string The URI fragment.
267
     */
268 4
    public function getFragment()
269
    {
270 4
        return (string) $this->fragment;
271
    }
272
273
    /**
274
     * Return an instance with the specified scheme.
275
     *
276
     * This method MUST retain the state of the current instance, and return
277
     * an instance that contains the specified scheme.
278
     *
279
     * Implementations MUST support the schemes "http" and "https" case
280
     * insensitively, and MAY accommodate other schemes if required.
281
     *
282
     * An empty scheme is equivalent to removing the scheme.
283
     *
284
     * @param string $scheme The scheme to use with the new instance.
285
     * @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...
286
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
287
     */
288
    public function withScheme($scheme)
289
    {
290
        // 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...
291
    }
292
293
    /**
294
     * Return an instance with the specified authority.
295
     *
296
     * This method MUST retain the state of the current instance, and return
297
     * an instance that contains the specified authority.
298
     *
299
     * Replacing the authority is equivalent to replacing or removing all authority components depending upon the
300
     * composition of the authority.
301
     *
302
     * An empty authority is equivalent to removing the authority and all authority components.
303
     *
304
     * @param string $authority The scheme to use with the new instance.
305
     * @return static A new instance with the specified authority.
306
     * @throws \InvalidArgumentException for invalid authorities.
307
     */
308 21
    public function withAuthority($authority)
309
    {
310 21
        $with_authority = new Authority($authority);
311
312 15
        $new_authority_uri = clone $this;
313
314 15
        $new_authority_uri->authority = $with_authority;
315
316 15
        $new_authority_uri->user_info = $with_authority->getUserInfo();
317 15
        $new_authority_uri->host      = $with_authority->getHost();
318 15
        $new_authority_uri->port      = $with_authority->getPort();
319
320 15
        return $new_authority_uri;
321
    }
322
323
    /**
324
     * Return an instance with the specified user information.
325
     *
326
     * This method MUST retain the state of the current instance, and return
327
     * an instance that contains the specified user information.
328
     *
329
     * Password is optional, but the user information MUST include the
330
     * user; an empty string for the user is equivalent to removing user
331
     * information.
332
     *
333
     * @param string $user The user name to use for authority.
334
     * @param null|string $password The password associated with $user.
335
     * @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...
336
     */
337
    public function withUserInfo($user, $password = null)
338
    {
339
        // 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...
340
    }
341
342
    /**
343
     * Return an instance with the specified host.
344
     *
345
     * This method MUST retain the state of the current instance, and return
346
     * an instance that contains the specified host.
347
     *
348
     * An empty host value is equivalent to removing the host.
349
     *
350
     * @param string $host The hostname to use with the new instance.
351
     * @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...
352
     * @throws \InvalidArgumentException for invalid hostnames.
353
     */
354
    public function withHost($host)
355
    {
356
        // 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...
357
    }
358
359
    /**
360
     * Return an instance with the specified port.
361
     *
362
     * This method MUST retain the state of the current instance, and return
363
     * an instance that contains the specified port.
364
     *
365
     * Implementations MUST raise an exception for ports outside the
366
     * established TCP and UDP port ranges.
367
     *
368
     * A null value provided for the port is equivalent to removing the port
369
     * information.
370
     *
371
     * @param null|int $port The port to use with the new instance; a null value
372
     *     removes the port information.
373
     * @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...
374
     * @throws \InvalidArgumentException for invalid ports.
375
     */
376
    public function withPort($port)
377
    {
378
        // 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...
379
    }
380
381
    /**
382
     * Return an instance with the specified path.
383
     *
384
     * This method MUST retain the state of the current instance, and return
385
     * an instance that contains the specified path.
386
     *
387
     * The path can either be empty or absolute (starting with a slash) or
388
     * rootless (not starting with a slash). Implementations MUST support all
389
     * three syntaxes.
390
     *
391
     * If the path is intended to be domain-relative rather than path relative then
392
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
393
     * are assumed to be relative to some base path known to the application or
394
     * consumer.
395
     *
396
     * Users can provide both encoded and decoded path characters.
397
     * Implementations ensure the correct encoding as outlined in getPath().
398
     *
399
     * @throws \InvalidArgumentException for invalid paths and invalid path/authority combinations.
400
     *
401
     * @param string $path The path to use with the new instance.
402
     * @return static A new instance with the specified path.
403
     */
404 18
    public function withPath($path)
405
    {
406 18
        if (!Path::validatePathAgainstAuthority($path, $this->authority)) {
407 1
            throw new \InvalidArgumentException("Cannot create a URI with an authority given a rootless path");
408
        }
409
410 11
        $with_path = new Path($path);
411
412 11
        $new_path_uri = clone $this;
413
414 11
        $new_path_uri->path = $with_path;
415
416 11
        $new_path_uri->uri_parts["path"] = $path;
417
418 11
        return $new_path_uri;
419
    }
420
421
    /**
422
     * Return an instance with the specified query string.
423
     *
424
     * This method MUST retain the state of the current instance, and return
425
     * an instance that contains the specified query string.
426
     *
427
     * Users can provide both encoded and decoded query characters.
428
     * Implementations ensure the correct encoding as outlined in getQuery().
429
     *
430
     * An empty query string value is equivalent to removing the query string.
431
     *
432
     * @param string $query The query string to use with the new instance.
433
     * @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...
434
     * @throws \InvalidArgumentException for invalid query strings.
435
     */
436
    public function withQuery($query)
437
    {
438
        // 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...
439
    }
440
441
    /**
442
     * Return an instance with the specified URI fragment.
443
     *
444
     * This method MUST retain the state of the current instance, and return
445
     * an instance that contains the specified URI fragment.
446
     *
447
     * Users can provide both encoded and decoded fragment characters.
448
     * Implementations ensure the correct encoding as outlined in getFragment().
449
     *
450
     * An empty fragment value is equivalent to removing the fragment.
451
     *
452
     * @param string $fragment The fragment to use with the new instance.
453
     * @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...
454
     */
455
    public function withFragment($fragment)
456
    {
457
        // 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...
458
    }
459
460
    /**
461
     * Return the string representation as a URI reference.
462
     *
463
     * Depending on which components of the URI are present, the resulting
464
     * string is either a full URI or relative reference according to RFC 3986,
465
     * Section 4.1. The method concatenates the various components of the URI,
466
     * using the appropriate delimiters:
467
     *
468
     * - If a scheme is present, it MUST be suffixed by ":".
469
     * - If an authority is present, it MUST be prefixed by "//".
470
     * - The path can be concatenated without delimiters. But there are two
471
     *   cases where the path has to be adjusted to make the URI reference
472
     *   valid as PHP does not allow to throw an exception in __toString():
473
     *     - If the path is rootless and an authority is present, the path MUST
474
     *       be prefixed by "/".
475
     *     - If the path is starting with more than one "/" and no authority is
476
     *       present, the starting slashes MUST be reduced to one.
477
     * - If a query is present, it MUST be prefixed by "?".
478
     * - If a fragment is present, it MUST be prefixed by "#".
479
     *
480
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
481
     * @return string
482
     */
483 9
    public function __toString()
484
    {
485 9
        return $this->toString();
486
    }
487
488
    /**
489
     * Converts a given array of URI parts to a string according to the specification of the __toString magic method
490
     *
491
     * @see Uri::__toString
492
     *
493
     * @return string           The string combined from the array of URI parts
494
     */
495 9
    private function toString()
496
    {
497 9
        $uri_string = "";
498
499 9
        $uri_string .= $this->scheme->toUriString();
500
501 9
        $uri_string .= $this->authority->toUriString($this->scheme);
502
503 9
        $uri_string .= $this->path->toUriString($this->authority);
504
505 9
        $uri_string .= $this->query->toUriString();
506
507 9
        $uri_string .= $this->fragment->toUriString();
508
509 9
        return $uri_string;
510
    }
511
512
    /**
513
     * Splits a string URI into its component parts, returning true if the URI string matches a valid URI's syntax
514
     * and false if the URI string does not
515
     *
516
     * @param string $uri   The URI string to be decomposed
517
     * @return bool         Returns true if the URI string matches a valid URI's syntax
518
     *                      Returns false otherwise
519
     */
520 72
    private function explodeUri($uri)
521
    {
522 72
        $reg_start     = '/^';
523 72
        $scheme_part   = '(?:(?\'scheme\'[A-Za-z0-9][^\/\?#:]+):)?';
524 72
        $hier_part     = '(?\'hier_part\'[^\?#]+)?';
525 72
        $query_part    = '(?:\?(?\'query\'[^#]*))?';
526 72
        $fragment_part = '(?:#(?\'fragment\'.*))?';
527 72
        $reg_end       = '/';
528
529 72
        $uri_syntax = $reg_start . $scheme_part . $hier_part . $query_part . $fragment_part . $reg_end;
530
531 72
        $uri_valid = preg_match($uri_syntax, $uri, $parts);
532
533 72
        $this->uri_parts = array_merge($this->uri_parts, $parts); //overwriting default values with matches
534
535 72
        $this->explodeHierParts($this->uri_parts["hier_part"]);
536
537 72
        $this->parseExplosions();
538
539 72
        $this->sanitizeUriPartsArray();
540
541 72
        return (bool) $uri_valid;
542
    }
543
544
    /**
545
     * Splits URI hierarchy data into authority and path data.
546
     *
547
     * @param string $hier_part     The hierarchy part of a URI to be decomposed
548
     * @return void
549
     */
550 72
    private function explodeHierParts($hier_part)
551
    {
552
        $hier_parts = array(
553 72
            "authority" => "",
554 72
            "path"      => "",
555 72
        );
556
557 72
        $reg_start      = '/^';
558 72
        $authority_part = '(?:(?:\/\/)(?\'authority\'.[^\/]+))?';
559 72
        $path_part      = '(?\'path\'.+)?';
560 72
        $reg_end        = '/';
561
562 72
        $hier_part_syntax = $reg_start . $authority_part . $path_part . $reg_end;
563
564 72
        preg_match($hier_part_syntax, $hier_part, $matches);
565
566 72
        $hier_parts = array_merge($hier_parts, $matches);
567
568 72
        $this->uri_parts = array_merge($this->uri_parts, $hier_parts);
569 72
    }
570
571
    /**
572
     * Parses the results of exploding URI parts.  Creates component objects and fills in uri_parts array with
573
     * sub-component data.
574
     *
575
     * @return void
576
     */
577 72
    private function parseExplosions()
578
    {
579 72
        $this->scheme    = new Scheme($this->uri_parts["scheme"]);
580 72
        $this->query     = new Query($this->uri_parts["query"]);
581 72
        $this->fragment  = new Fragment($this->uri_parts["fragment"]);
582 72
        $this->authority = new Authority($this->uri_parts["authority"]);
583 72
        $this->user_info = $this->authority->getUserInfo();
584 72
        $this->host      = $this->authority->getHost();
585 72
        $this->port      = $this->authority->getPort();
586 72
        $this->path      = new Path($this->uri_parts["path"]);
587
588 72
        $this->uri_parts["user_info"] = (string) $this->user_info;
589 72
        $this->uri_parts["host"]      = (string) $this->host;
590 72
        $this->uri_parts["port"]      = $this->port->getPort();
591 72
    }
592
593
    /**
594
     * Sanitizes the URI component array by removing redundant key/value pairs
595
     *
596
     * @return void
597
     */
598 72
    private function sanitizeUriPartsArray()
599
    {
600 72
        $uri_part_array = new ArrayHelper($this->uri_parts);
601
602 72
        $this->uri_parts = $uri_part_array->removeNumericKeys();
603 72
    }
604
}
605