Completed
Push — master ( 028100...eaca17 )
by Derek
02:07
created

Uri::explodeUri()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 1

Importance

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