HttpUri::getComponents()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 0
dl 0
loc 10
ccs 8
cts 8
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of the login-cidadao project or it's bundles.
4
 *
5
 * (c) Guilherme Donato <guilhermednt on github>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace LoginCidadao\RemoteClaimsBundle\Model;
12
13
use Psr\Http\Message\UriInterface;
14
15
class HttpUri implements UriInterface
16
{
17
    /**
18
     * Pattern extracted from https://regex101.com/r/BGxJ6n/1/codegen?language=php
19
     *
20
     * TODO: use the pattern from LoginCidadao\ValidationBundle\Validator\Constraints\UriValidator when it's merged
21
     */
22
    const RFC3986 = '/(?#URI)^(?#
23
    Scheme  )(?<scheme>https|http):(?#
24
    HeirPart)(?<HierPart>\/\/(?#
25
        Authority)(?<Authority>(?#
26
            UserInfo)((?<userInfo>(\%[0-9a-f][0-9a-f]|[a-z0-9\-\.\_\~]|[\!\$\&\'\(\)\*\+\,\;\=]|\:)*)\@)?(?#
27
            Host    )(?<host>(?#
28
                IP Literal)\[((?#
29
                    IPv6 Address     )(?<IPv6>((?<IPv6_1_R_H16>[0-9a-f]{1,4})\:){6,6}(?<IPV6_1_R_LS32>((?<IPV6_1_R_LS32_IPV4_DEC_OCTET>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3,3}(?<IPV6_1_R_LS32_IPV4_DEC_OCTET_>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?<IPV6_1_R_LS32_H16_1>[0-9a-f]{1,4})\:(?<IPV6_1_R_LS32_H16_2>[0-9a-f]{1,4}))|\:\:((?<IPV6_2_R_H16>[0-9a-f]{1,4})\:){5,5}(?<IPV6_2_R_LS32>((?<IPV6_2_R_LS32_IPV4_DEC_OCTET>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3,3}(?<IPV6_2_R_LS32_IPV4_DEC_OCTET_>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?<IPV6_2_R_LS32_H16_1>[0-9a-f]{1,4})\:(?<IPV6_2_R_LS32_H16_2>[0-9a-f]{1,4}))|(?<IPV6_3_L_H16>[0-9a-f]{1,4})?\:\:((?<IPV6_3_R_H16>[0-9a-f]{1,4})\:){4,4}(?<IPV6_3_R_LS32>((?<IPV6_3_R_LS32_IPV4_DEC_OCTET>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3,3}(?<IPV6_3_R_LS32_IPV4_DEC_OCTET_>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?<IPV6_3_R_LS32_H16_1>[0-9a-f]{1,4})\:(?<IPV6_3_R_LS32_H16_2>[0-9a-f]{1,4}))|(((?<IPV6_4_L_H16_REPEAT>[0-9a-f]{1,4})\:)?(?<IPV6_4_L_H16>[0-9a-f]{1,4}))?\:\:((?<IPV6_4_R_H16>[0-9a-f]{1,4})\:){3,3}(?<IPV6_4_R_LS32>((?<IPV6_4_R_LS32_IPV4_DEC_OCTET>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3,3}(?<IPV6_4_R_LS32_IPV4_DEC_OCTET_>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?<IPV6_4_R_LS32_H16_1>[0-9a-f]{1,4})\:(?<IPV6_4_R_LS32_H16_2>[0-9a-f]{1,4}))|(((?<IPV6_5_L_H16_REPEAT>[0-9a-f]{1,4})\:){0,2}(?<IPV6_5_L_H16>[0-9a-f]{1,4}))?\:\:((?<IPV6_5_R_H16>[0-9a-f]{1,4})\:){2,2}(?<IPV6_5_R_LS32>((?<IPV6_5_R_LS32_IPV4_DEC_OCTET>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3,3}(?<IPV6_5_R_LS32_IPV4_DEC_OCTET_>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?<IPV6_5_R_LS32_H16_1>[0-9a-f]{1,4})\:(?<IPV6_5_R_LS32_H16_2>[0-9a-f]{1,4}))|(((?<IPV6_6_L_H16_REPEAT>[0-9a-f]{1,4})\:){0,3}(?<IPV6_6_L_H16>[0-9a-f]{1,4}))?\:\:(?<IPV6_6_R_H16>[0-9a-f]{1,4})\:(?<IPV6_6_R_LS32>((?<IPV6_6_R_LS32_IPV4_DEC_OCTET>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3,3}(?<IPV6_6_R_LS32_IPV4_DEC_OCTET_>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?<IPV6_6_R_LS32_H16_1>[0-9a-f]{1,4})\:(?<IPV6_6_R_LS32_H16_2>[0-9a-f]{1,4}))|(((?<IPV6_7_L_H16_REPEAT>[0-9a-f]{1,4})\:){0,4}(?<IPV6_7_L_H16>[0-9a-f]{1,4}))?\:\:(?<IPV6_7_R_LS32>((?<IPV6_7_R_LS32_IPV4_DEC_OCTET>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3,3}(?<IPV6_7_R_LS32_IPV4_DEC_OCTET_>[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?<IPV6_7_R_LS32_H16_1>[0-9a-f]{1,4})\:(?<IPV6_7_R_LS32_H16_2>[0-9a-f]{1,4}))|(((?<IPV6_8_L_H16_REPEAT>[0-9a-f]{1,4})\:){0,5}(?<IPV6_8_L_H16>[0-9a-f]{1,4}))?\:\:(?<IPV6_8_R_H16>[0-9a-f]{1,4})|(((?<IPV6_9_L_H16_REPEAT>[0-9a-f]{1,4})\:){0,6}(?<IPV6_9_L_H16>[0-9a-f]{1,4}))?\:\:)|(?#
30
                    IPvFuture Address)v[a-f0-9]+\.([a-z0-9\-\.\_\~]|[\!\$\&\'\(\)\*\+\,\;\=]|\:)+(?#
31
                ))\]|(?#
32
                IPv4 Address)(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3,3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?#
33
                RegName)([a-z0-9\-\.\_\~]|\%[0-9a-f][0-9a-f]|[\!\$\&\'\(\)\*\+\,\;\=])*(?#
34
            ))(?#
35
            Port    )(:(?<port>[0-9]+))?(?#
36
        ))(?#
37
        Path     )(?<path>(\/([a-z0-9\-\.\_\~\!\$\&\'\(\)\*\+\,\;\=\:\@]|(%[a-f0-9]{2,2}))*)*)(?#
38
    ))(?#
39
    Query   )(?<query>\?([a-z0-9\-\.\_\~\!\$\&\'\(\)\*\+\,\;\=\:\@\/\?]|(%[a-f0-9]{2,2}))*)?(?#
40
    Fragment)(?<fragment>#([a-z0-9\-\.\_\~\!\$\&\'\(\)\*\+\,\;\=\:\@\/\?]|(%[a-f0-9]{2,2}))*)?(?#
41
)$/imX';
42
43
    /** @var string */
44
    private $scheme = '';
45
46
    /** @var string */
47
    private $userInfo = '';
48
49
    /** @var string */
50
    private $host = '';
51
52
    /** @var null|int */
53
    private $port = null;
54
55
    /** @var string */
56
    private $path = '';
57
58
    /** @var string */
59
    private $query = '';
60
61
    /** @var string */
62
    private $fragment = '';
63
64
    /**
65
     * Retrieve the scheme component of the URI.
66
     *
67
     * If no scheme is present, this method MUST return an empty string.
68
     *
69
     * The value returned MUST be normalized to lowercase, per RFC 3986
70
     * Section 3.1.
71
     *
72
     * The trailing ":" character is not part of the scheme and MUST NOT be
73
     * added.
74
     *
75
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
76
     * @return string The URI scheme.
77
     */
78 15
    public function getScheme()
79
    {
80 15
        return $this->scheme;
81
    }
82
83
    /**
84
     * Retrieve the authority component of the URI.
85
     *
86
     * If no authority information is present, this method MUST return an empty
87
     * string.
88
     *
89
     * The authority syntax of the URI is:
90
     *
91
     * <pre>
92
     * [user-info@]host[:port]
93
     * </pre>
94
     *
95
     * If the port component is not set or is the standard port for the current
96
     * scheme, it SHOULD NOT be included.
97
     *
98
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
99
     * @return string The URI authority, in "[user-info@]host[:port]" format.
100
     */
101 7
    public function getAuthority()
102
    {
103 7
        $userInfoHost = implode('@', array_filter([$this->getUserInfo(), $this->getHost()]));
104
105 7
        return implode(':', array_filter([$userInfoHost, $this->getPort()]));
106
    }
107
108
    /**
109
     * Retrieve the user information component of the URI.
110
     *
111
     * If no user information is present, this method MUST return an empty
112
     * string.
113
     *
114
     * If a user is present in the URI, this will return that value;
115
     * additionally, if the password is also present, it will be appended to the
116
     * user value, with a colon (":") separating the values.
117
     *
118
     * The trailing "@" character is not part of the user information and MUST
119
     * NOT be added.
120
     *
121
     * @return string The URI user information, in "username[:password]" format.
122
     */
123 7
    public function getUserInfo()
124
    {
125 7
        return $this->userInfo;
126
    }
127
128
    /**
129
     * Retrieve the host component of the URI.
130
     *
131
     * If no host is present, this method MUST return an empty string.
132
     *
133
     * The value returned MUST be normalized to lowercase, per RFC 3986
134
     * Section 3.2.2.
135
     *
136
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
137
     * @return string The URI host.
138
     */
139 15
    public function getHost()
140
    {
141 15
        return $this->host;
142
    }
143
144
    /**
145
     * Retrieve the port component of the URI.
146
     *
147
     * If a port is present, and it is non-standard for the current scheme,
148
     * this method MUST return it as an integer. If the port is the standard port
149
     * used with the current scheme, this method SHOULD return null.
150
     *
151
     * If no port is present, and no scheme is present, this method MUST return
152
     * a null value.
153
     *
154
     * If no port is present, but a scheme is present, this method MAY return
155
     * the standard port for that scheme, but SHOULD return null.
156
     *
157
     * @return null|int The URI port.
158
     */
159 13
    public function getPort()
160
    {
161 13
        return $this->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 7
    public function getPath()
190
    {
191 7
        return $this->path;
192
    }
193
194
    /**
195
     * Retrieve the query string of the URI.
196
     *
197
     * If no query string is present, this method MUST return an empty string.
198
     *
199
     * The leading "?" character is not part of the query and MUST NOT be
200
     * added.
201
     *
202
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
203
     * any characters. To determine what characters to encode, please refer to
204
     * RFC 3986, Sections 2 and 3.4.
205
     *
206
     * As an example, if a value in a key/value pair of the query string should
207
     * include an ampersand ("&") not intended as a delimiter between values,
208
     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
209
     *
210
     * @see https://tools.ietf.org/html/rfc3986#section-2
211
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
212
     * @return string The URI query string.
213
     */
214 7
    public function getQuery()
215
    {
216 7
        return $this->query;
217
    }
218
219
    /**
220
     * Retrieve the fragment component of the URI.
221
     *
222
     * If no fragment is present, this method MUST return an empty string.
223
     *
224
     * The leading "#" character is not part of the fragment and MUST NOT be
225
     * added.
226
     *
227
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
228
     * any characters. To determine what characters to encode, please refer to
229
     * RFC 3986, Sections 2 and 3.5.
230
     *
231
     * @see https://tools.ietf.org/html/rfc3986#section-2
232
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
233
     * @return string The URI fragment.
234
     */
235 7
    public function getFragment()
236
    {
237 7
        return $this->fragment;
238
    }
239
240
    /**
241
     * Return an instance with the specified scheme.
242
     *
243
     * This method MUST retain the state of the current instance, and return
244
     * an instance that contains the specified scheme.
245
     *
246
     * Implementations MUST support the schemes "http" and "https" case
247
     * insensitively, and MAY accommodate other schemes if required.
248
     *
249
     * An empty scheme is equivalent to removing the scheme.
250
     *
251
     * @param string $scheme The scheme to use with the new instance.
252
     * @return static A new instance with the specified scheme.
253
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
254
     */
255 1
    public function withScheme($scheme)
256
    {
257 1
        return $this->with('scheme', $scheme);
258
    }
259
260
    /**
261
     * Return an instance with the specified user information.
262
     *
263
     * This method MUST retain the state of the current instance, and return
264
     * an instance that contains the specified user information.
265
     *
266
     * Password is optional, but the user information MUST include the
267
     * user; an empty string for the user is equivalent to removing user
268
     * information.
269
     *
270
     * @param string $user The user name to use for authority.
271
     * @param null|string $password The password associated with $user.
272
     * @return static A new instance with the specified user information.
273
     */
274 1
    public function withUserInfo($user, $password = null)
275
    {
276 1
        $userInfo = implode(':', [$user, $password]);
277
278 1
        return $this->with('userInfo', $userInfo);
279
    }
280
281
    /**
282
     * Return an instance with the specified host.
283
     *
284
     * This method MUST retain the state of the current instance, and return
285
     * an instance that contains the specified host.
286
     *
287
     * An empty host value is equivalent to removing the host.
288
     *
289
     * @param string $host The hostname to use with the new instance.
290
     * @return static A new instance with the specified host.
291
     * @throws \InvalidArgumentException for invalid hostnames.
292
     */
293 1
    public function withHost($host)
294
    {
295 1
        return $this->with('host', $host);
296
    }
297
298
    /**
299
     * Return an instance with the specified port.
300
     *
301
     * This method MUST retain the state of the current instance, and return
302
     * an instance that contains the specified port.
303
     *
304
     * Implementations MUST raise an exception for ports outside the
305
     * established TCP and UDP port ranges.
306
     *
307
     * A null value provided for the port is equivalent to removing the port
308
     * information.
309
     *
310
     * @param null|int $port The port to use with the new instance; a null value
311
     *     removes the port information.
312
     * @return static A new instance with the specified port.
313
     * @throws \InvalidArgumentException for invalid ports.
314
     */
315 1
    public function withPort($port)
316
    {
317 1
        return $this->with('port', $port);
318
    }
319
320
    /**
321
     * Return an instance with the specified path.
322
     *
323
     * This method MUST retain the state of the current instance, and return
324
     * an instance that contains the specified path.
325
     *
326
     * The path can either be empty or absolute (starting with a slash) or
327
     * rootless (not starting with a slash). Implementations MUST support all
328
     * three syntaxes.
329
     *
330
     * If the path is intended to be domain-relative rather than path relative then
331
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
332
     * are assumed to be relative to some base path known to the application or
333
     * consumer.
334
     *
335
     * Users can provide both encoded and decoded path characters.
336
     * Implementations ensure the correct encoding as outlined in getPath().
337
     *
338
     * @param string $path The path to use with the new instance.
339
     * @return static A new instance with the specified path.
340
     * @throws \InvalidArgumentException for invalid paths.
341
     */
342 1
    public function withPath($path)
343
    {
344 1
        return $this->with('path', $path);
345
    }
346
347
    /**
348
     * Return an instance with the specified query string.
349
     *
350
     * This method MUST retain the state of the current instance, and return
351
     * an instance that contains the specified query string.
352
     *
353
     * Users can provide both encoded and decoded query characters.
354
     * Implementations ensure the correct encoding as outlined in getQuery().
355
     *
356
     * An empty query string value is equivalent to removing the query string.
357
     *
358
     * @param string $query The query string to use with the new instance.
359
     * @return static A new instance with the specified query string.
360
     * @throws \InvalidArgumentException for invalid query strings.
361
     */
362 1
    public function withQuery($query)
363
    {
364 1
        return $this->with('query', $query);
365
    }
366
367
    /**
368
     * Return an instance with the specified URI fragment.
369
     *
370
     * This method MUST retain the state of the current instance, and return
371
     * an instance that contains the specified URI fragment.
372
     *
373
     * Users can provide both encoded and decoded fragment characters.
374
     * Implementations ensure the correct encoding as outlined in getFragment().
375
     *
376
     * An empty fragment value is equivalent to removing the fragment.
377
     *
378
     * @param string $fragment The fragment to use with the new instance.
379
     * @return static A new instance with the specified fragment.
380
     */
381 1
    public function withFragment($fragment)
382
    {
383 1
        return $this->with('fragment', $fragment);
384
    }
385
386
    /**
387
     * Return the string representation as a URI reference.
388
     *
389
     * Depending on which components of the URI are present, the resulting
390
     * string is either a full URI or relative reference according to RFC 3986,
391
     * Section 4.1. The method concatenates the various components of the URI,
392
     * using the appropriate delimiters:
393
     *
394
     * - If a scheme is present, it MUST be suffixed by ":".
395
     * - If an authority is present, it MUST be prefixed by "//".
396
     * - The path can be concatenated without delimiters. But there are two
397
     *   cases where the path has to be adjusted to make the URI reference
398
     *   valid as PHP does not allow to throw an exception in __toString():
399
     *     - If the path is rootless and an authority is present, the path MUST
400
     *       be prefixed by "/".
401
     *     - If the path is starting with more than one "/" and no authority is
402
     *       present, the starting slashes MUST be reduced to one.
403
     * - If a query is present, it MUST be prefixed by "?".
404
     * - If a fragment is present, it MUST be prefixed by "#".
405
     *
406
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
407
     * @return string
408
     */
409 7
    public function __toString()
410
    {
411 7
        $scheme = $this->getScheme();
412 7
        $authority = $this->getAuthority();
413 7
        $path = $this->getPath();
414 7
        $fragment = $this->getFragment() ? '#'.$this->getFragment() : '';
415 7
        $query = $this->getQuery() ? '?'.$this->getQuery() : '';
416
417 7
        return "{$scheme}://{$authority}{$path}{$query}{$fragment}";
418
    }
419
420
    /**
421
     * @param string $scheme
422
     * @return HttpUri
423
     */
424 15
    public function setScheme($scheme)
425
    {
426 15
        $this->scheme = $scheme;
427
428 15
        return $this;
429
    }
430
431
    /**
432
     * @param string $userInfo
433
     * @return HttpUri
434
     */
435 15
    public function setUserInfo($userInfo)
436
    {
437 15
        $this->userInfo = $userInfo;
438
439 15
        return $this;
440
    }
441
442
    /**
443
     * @param string $host
444
     * @return HttpUri
445
     */
446 15
    public function setHost($host)
447
    {
448 15
        $this->host = $host;
449
450 15
        return $this;
451
    }
452
453
    /**
454
     * @param int|null $port
455
     * @return HttpUri
456
     */
457 15
    public function setPort($port)
458
    {
459 15
        $this->port = $port;
460
461 15
        return $this;
462
    }
463
464
    /**
465
     * @param string $path
466
     * @return HttpUri
467
     */
468 15
    public function setPath($path)
469
    {
470 15
        $this->path = $path;
471
472 15
        return $this;
473
    }
474
475
    /**
476
     * @param string $query
477
     * @return HttpUri
478
     */
479 15
    public function setQuery($query)
480
    {
481 15
        $this->query = $query;
482
483 15
        return $this;
484
    }
485
486
    /**
487
     * @param string $fragment
488
     * @return HttpUri
489
     */
490 15
    public function setFragment($fragment)
491
    {
492 15
        $this->fragment = $fragment;
493
494 15
        return $this;
495
    }
496
497 18
    private static function regexDecomposeUri($uri)
498
    {
499 18
        if (!preg_match(self::RFC3986, $uri, $m)) {
500 8
            throw new \InvalidArgumentException("Invalid HTTP URI: {$uri}");
501
        }
502
503 13
        foreach ($m as $key => $value) {
504 13
            if (is_int($key)) {
505 13
                unset($m[$key]);
506
            }
507
        }
508
509 13
        return $m;
510
    }
511
512 13
    private static function sanitizeComponents($components)
513
    {
514 13
        $components['userInfo'] = preg_replace('/[@]$/', '', $components['userInfo']);
515 13
        $components['query'] = preg_replace('/^[?]/', '', $components['query']);
516 13
        $components['fragment'] = preg_replace('/^[#]/', '', $components['fragment']);
517 13
        $components['port'] = str_replace(':', '', $components['port']);
518 13
        if (!is_numeric($components['port'])) {
519 12
            $components['port'] = null;
520
        }
521
522 13
        return $components;
523
    }
524
525 18
    public static function parseUri($uri)
526
    {
527 18
        $components = self::getDefaultComponents();
528 18
        $allowedComponents = ['scheme', 'userInfo', 'host', 'port', 'path', 'query', 'fragment'];
529
530 18
        foreach (self::regexDecomposeUri($uri) as $component => $value) {
531 13
            if (array_search($component, $allowedComponents) !== false) {
532 13
                $components[$component] = $value;
533
            }
534
        }
535
536 13
        return self::sanitizeComponents($components);
537
    }
538
539 16
    public static function createFromString($uri)
540
    {
541 16
        $parts = self::parseUri($uri);
542
543 11
        return self::createFromComponents($parts);
544
    }
545
546 15
    public static function createFromComponents($parts)
547
    {
548
        // Set default values
549 15
        $parts = array_merge(self::getDefaultComponents(), $parts);
550
551 15
        $uri = (new HttpUri())
552 15
            ->setScheme($parts['scheme'])
553 15
            ->setUserInfo($parts['userInfo'])
554 15
            ->setHost($parts['host'])
555 15
            ->setPort($parts['port'])
556 15
            ->setPath($parts['path'])
557 15
            ->setQuery($parts['query'])
558 15
            ->setFragment($parts['fragment']);
559
560 15
        return $uri;
561
    }
562
563 21
    private static function getDefaultComponents()
564
    {
565
        return [
566 21
            'scheme' => '',
567
            'userInfo' => '',
568
            'host' => '',
569
            'port' => null,
570
            'path' => '',
571
            'query' => '',
572
            'fragment' => '',
573
        ];
574
    }
575
576 1
    private function getComponents()
577
    {
578
        return [
579 1
            'scheme' => $this->getScheme(),
580 1
            'userInfo' => $this->getUserInfo(),
581 1
            'host' => $this->getHost(),
582 1
            'port' => $this->getPort(),
583 1
            'path' => $this->getPath(),
584 1
            'query' => $this->getQuery(),
585 1
            'fragment' => $this->getFragment(),
586
        ];
587
    }
588
589 1
    private function with($component, $value)
590
    {
591 1
        $components = $this->getComponents();
592 1
        $components[$component] = $value;
593
594 1
        return self::createFromComponents($components);
595
    }
596
}
597