Completed
Push — master ( 56c359...717fd1 )
by Sebastian
04:15 queued 01:56
created

Uri::withHost()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Linna Psr7.
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2018, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna\Http\Message;
13
14
use InvalidArgumentException;
15
use Psr\Http\Message\UriInterface;
16
17
/**
18
 * Psr7 Uri Implementation.
19
 */
20
class Uri implements UriInterface
21
{
22
    use UriTrait;
23
24
    /**
25
     * @var array Standard schemes.
26
     */
27
    protected $standardSchemes = [
28
        'http'  => 80,
29
        'https' => 443,
30
    ];
31
32
    /**
33
     * @var array Url description.
34
     */
35
    protected $url = [
36
        'scheme'   => '',
37
        'host'     => '',
38
        'port'     => 0,
39
        'user'     => '',
40
        'pass'     => '',
41
        'path'     => '',
42
        'query'    => '',
43
        'fragment' => '',
44
    ];
45
46
    /**
47
     * Constructor.
48
     *
49
     * @param string $uri
50
     *
51
     * @throws InvalidArgumentException
52
     */
53 82
    public function __construct(string $uri)
54
    {
55 82
        if (($parsedUrl = parse_url($uri)) === false) {
56 1
            throw new InvalidArgumentException(__CLASS__.': Bad URI provided for '.__METHOD__);
57
        }
58
59 81
        $this->url = array_merge($this->url, $parsedUrl);
60 81
    }
61
62
    /**
63
     * Retrieve the scheme component of the URI.
64
     *
65
     * If no scheme is present, this method MUST return an empty string.
66
     *
67
     * The value returned MUST be normalized to lowercase, per RFC 3986
68
     * Section 3.1.
69
     *
70
     * The trailing ":" character is not part of the scheme and MUST NOT be
71
     * added.
72
     *
73
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
74
     *
75
     * @return string The URI scheme.
76
     */
77 2
    public function getScheme(): string
78
    {
79 2
        return strtolower($this->url['scheme']);
80
    }
81
82
    /**
83
     * Retrieve the authority component of the URI.
84
     *
85
     * If no authority information is present, this method MUST return an empty
86
     * string.
87
     *
88
     * The authority syntax of the URI is:
89
     *
90
     * <pre>
91
     * [user-info@]host[:port]
92
     * </pre>
93
     *
94
     * If the port component is not set or is the standard port for the current
95
     * scheme, it SHOULD NOT be included.
96
     *
97
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
98
     *
99
     * @return string The URI authority, in "[user-info@]host[:port]" format.
100
     */
101 26
    public function getAuthority(): string
102
    {
103 26
        if ($this->url['host'] === '') {
104 5
            return '';
105
        }
106
107 21
        $authority = $this->url['host'];
108
109 21
        if ($this->url['user'] !== '') {
110 18
            $authority = $this->getUserInfo().'@'.$authority;
111
        }
112
113 21
        if ($this->url['port'] !== 0) {
114 16
            $authority .= ':'.$this->url['port'];
115
        }
116
117 21
        return $authority;
118
    }
119
120
    /**
121
     * Retrieve the user information component of the URI.
122
     *
123
     * If no user information is present, this method MUST return an empty
124
     * string.
125
     *
126
     * If a user is present in the URI, this will return that value;
127
     * additionally, if the password is also present, it will be appended to the
128
     * user value, with a colon (":") separating the values.
129
     *
130
     * The trailing "@" character is not part of the user information and MUST
131
     * NOT be added.
132
     *
133
     * @return string The URI user information, in "username[:password]" format.
134
     */
135 21
    public function getUserInfo(): string
136
    {
137 21
        $user = $this->url['user'];
138
139 21
        if ($this->url['pass'] !== '' && $this->url['pass'] !== null) {
140 17
            $user .= ':'.$this->url['pass'];
141
        }
142
143 21
        return ($user !== '') ? $user : '';
144
    }
145
146
    /**
147
     * Retrieve the host component of the URI.
148
     *
149
     * If no host is present, this method MUST return an empty string.
150
     *
151
     * The value returned MUST be normalized to lowercase, per RFC 3986
152
     * Section 3.2.2.
153
     *
154
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
155
     *
156
     * @return string The URI host.
157
     */
158 2
    public function getHost(): string
159
    {
160 2
        return strtolower($this->url['host']);
161
    }
162
163
    /**
164
     * Retrieve the port component of the URI.
165
     *
166
     * If a port is present, and it is non-standard for the current scheme,
167
     * this method MUST return it as an integer. If the port is the standard port
168
     * used with the current scheme, this method SHOULD return null.
169
     *
170
     * If no port is present, and no scheme is present, this method MUST return
171
     * a zero value.
172
     *
173
     * If no port is present, but a scheme is present, this method MAY return
174
     * the standard port for that scheme, but SHOULD return zero.
175
     *
176
     * @return int The URI port.
177
     */
178 8
    public function getPort(): int
179
    {
180 8
        $scheme = $this->url['scheme'];
181 8
        $port = $this->url['port'];
182
183 8
        $standardPort = $this->checkStandardPortForCurretScheme($scheme, $port, $this->standardSchemes);
184 8
        $standardScheme = array_key_exists($scheme, $this->standardSchemes);
185
186
        //scheme present and port standard - return 0
187
        //scheme present and port non standard - return port
188 8
        if ($standardPort && $standardScheme) {
189 1
            return $this->getPortForStandardScheme($standardPort, $port);
190
        }
191
192
        //scheme present and standard, port non present - return port
193
        //scheme non standard, port present - return port
194 7
        return $this->getNonStandardPort($port, $scheme, $standardScheme, $this->standardSchemes);
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
     *
221
     * @return string The URI path.
222
     */
223 20
    public function getPath(): string
224
    {
225 20
        return $this->url['path'];
226
    }
227
228
    /**
229
     * Retrieve the query string of the URI.
230
     *
231
     * If no query string is present, this method MUST return an empty string.
232
     *
233
     * The leading "?" character is not part of the query and MUST NOT be
234
     * added.
235
     *
236
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
237
     * any characters. To determine what characters to encode, please refer to
238
     * RFC 3986, Sections 2 and 3.4.
239
     *
240
     * As an example, if a value in a key/value pair of the query string should
241
     * include an ampersand ("&") not intended as a delimiter between values,
242
     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
243
     *
244
     * @see https://tools.ietf.org/html/rfc3986#section-2
245
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
246
     *
247
     * @return string The URI query string.
248
     */
249 4
    public function getQuery(): string
250
    {
251 4
        return $this->url['query'];
252
    }
253
254
    /**
255
     * Retrieve the fragment component of the URI.
256
     *
257
     * If no fragment is present, this method MUST return an empty string.
258
     *
259
     * The leading "#" character is not part of the fragment and MUST NOT be
260
     * added.
261
     *
262
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
263
     * any characters. To determine what characters to encode, please refer to
264
     * RFC 3986, Sections 2 and 3.5.
265
     *
266
     * @see https://tools.ietf.org/html/rfc3986#section-2
267
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
268
     *
269
     * @return string The URI fragment.
270
     */
271 4
    public function getFragment(): string
272
    {
273 4
        return $this->url['fragment'];
274
    }
275
276
    /**
277
     * Return an instance with the specified scheme.
278
     *
279
     * This method MUST retain the state of the current instance, and return
280
     * an instance that contains the specified scheme.
281
     *
282
     * Implementations MUST support the schemes "http" and "https" case
283
     * insensitively, and MAY accommodate other schemes if required.
284
     *
285
     * An empty scheme is equivalent to removing the scheme.
286
     *
287
     * @param string $scheme The scheme to use with the new instance.
288
     *
289
     * @return static A new instance with the specified scheme.
290
     *
291
     * @throws InvalidArgumentException for invalid or unsupported schemes.
292
     */
293 2
    public function withScheme(string $scheme): UriInterface
294
    {
295 2
        if (!array_key_exists($scheme, $this->standardSchemes)) {
296 1
            throw new InvalidArgumentException(__CLASS__.': Invalid or unsupported scheme provided for '.__METHOD__);
297
        }
298
299 1
        $new = clone $this;
300 1
        $new->url['scheme'] = $scheme;
301
302 1
        return $new;
303
    }
304
305
    /**
306
     * Return an instance with the specified user information.
307
     *
308
     * This method MUST retain the state of the current instance, and return
309
     * an instance that contains the specified user information.
310
     *
311
     * Password is optional, but the user information MUST include the
312
     * user; an empty string for the user is equivalent to removing user
313
     * information.
314
     *
315
     * @param string $user The user name to use for authority.
316
     * @param string $password The password associated with $user.
317
     *
318
     * @return static A new instance with the specified user information.
319
     */
320 3
    public function withUserInfo(string $user, string $password = ''): UriInterface
321
    {
322 3
        $new = clone $this;
323 3
        $new->url['user'] = $user;
324 3
        $new->url['pass'] = $password;
325
326 3
        return $new;
327
    }
328
329
    /**
330
     * Return an instance with the specified host.
331
     *
332
     * This method MUST retain the state of the current instance, and return
333
     * an instance that contains the specified host.
334
     *
335
     * An empty host value is equivalent to removing the host.
336
     *
337
     * @param string $host The hostname to use with the new instance.
338
     *
339
     * @return static A new instance with the specified host.
340
     *
341
     * @throws InvalidArgumentException for invalid hostnames.
342
     */
343 2
    public function withHost(string $host): UriInterface
344
    {
345 2
        if (filter_var($host, \FILTER_VALIDATE_DOMAIN, \FILTER_FLAG_HOSTNAME) === false) {
346 1
            throw new InvalidArgumentException(__CLASS__.': Invalid host provided for '.__METHOD__);
347
        }
348
349 1
        $new = clone $this;
350 1
        $new->url['host'] = $host;
351
352 1
        return $new;
353
    }
354
355
    /**
356
     * Return an instance with the specified port.
357
     *
358
     * This method MUST retain the state of the current instance, and return
359
     * an instance that contains the specified port.
360
     *
361
     * Implementations MUST raise an exception for ports outside the
362
     * established TCP and UDP port ranges.
363
     *
364
     * A zero value provided for the port is equivalent to removing the port
365
     * information.
366
     *
367
     * @param int $port The port to use with the new instance; a zero value
368
     *                  removes the port information.
369
     *
370
     * @return static A new instance with the specified port.
371
     *
372
     * @throws InvalidArgumentException for invalid ports.
373
     */
374 4
    public function withPort(int $port = 0): UriInterface
375
    {
376 4
        if ($port !== 0 && ($port < 1 || $port > 65535)) {
377 2
            throw new InvalidArgumentException(__CLASS__.': Invalid port ('.$port.') number provided for '.__METHOD__);
378
        }
379
380 2
        $new = clone $this;
381 2
        $new->url['port'] = $port;
382
383 2
        return $new;
384
    }
385
386
    /**
387
     * Return an instance with the specified path.
388
     *
389
     * This method MUST retain the state of the current instance, and return
390
     * an instance that contains the specified path.
391
     *
392
     * The path can either be empty or absolute (starting with a slash) or
393
     * rootless (not starting with a slash). Implementations MUST support all
394
     * three syntaxes.
395
     *
396
     * If the path is intended to be domain-relative rather than path relative then
397
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
398
     * are assumed to be relative to some base path known to the application or
399
     * consumer.
400
     *
401
     * Users can provide both encoded and decoded path characters.
402
     * Implementations ensure the correct encoding as outlined in getPath().
403
     *
404
     * @param string $path The path to use with the new instance.
405
     *
406
     * @return static A new instance with the specified path.
407
     *
408
     * @throws InvalidArgumentException for invalid paths.
409
     */
410 3
    public function withPath(string $path): UriInterface
411
    {
412 3
        if (strpos($path, '?') !== false) {
413 1
            throw new InvalidArgumentException(__CLASS__.': Invalid path provided; must not contain a query string');
414
        }
415
416 2
        if (strpos($path, '#') !== false) {
417 1
            throw new InvalidArgumentException(__CLASS__.': Invalid path provided; must not contain a URI fragment');
418
        }
419
420 1
        $new = clone $this;
421 1
        $new->url['path'] = $path;
422
423 1
        return $new;
424
    }
425
426
    /**
427
     * Return an instance with the specified query string.
428
     *
429
     * This method MUST retain the state of the current instance, and return
430
     * an instance that contains the specified query string.
431
     *
432
     * Users can provide both encoded and decoded query characters.
433
     * Implementations ensure the correct encoding as outlined in getQuery().
434
     *
435
     * An empty query string value is equivalent to removing the query string.
436
     *
437
     * @param string $query The query string to use with the new instance.
438
     *
439
     * @return static A new instance with the specified query string.
440
     *
441
     * @throws InvalidArgumentException for invalid query strings.
442
     */
443 4
    public function withQuery(string $query): UriInterface
444
    {
445 4
        if (strpos($query, '#') !== false) {
446 1
            throw new InvalidArgumentException(__CLASS__.': Query string must not include a URI fragment');
447
        }
448
449 3
        $new = clone $this;
450 3
        $new->url['query'] = (strpos($query, '?') !== false) ? substr($query, 1) : $query;
451
452 3
        return $new;
453
    }
454
455
    /**
456
     * Return an instance with the specified URI fragment.
457
     *
458
     * This method MUST retain the state of the current instance, and return
459
     * an instance that contains the specified URI fragment.
460
     *
461
     * Users can provide both encoded and decoded fragment characters.
462
     * Implementations ensure the correct encoding as outlined in getFragment().
463
     *
464
     * An empty fragment value is equivalent to removing the fragment.
465
     *
466
     * @param string $fragment The fragment to use with the new instance.
467
     *
468
     * @return static A new instance with the specified fragment.
469
     */
470 3
    public function withFragment(string $fragment): UriInterface
471
    {
472 3
        $new = clone $this;
473 3
        $new->url['fragment'] = (strpos($fragment, '#') !== false) ? substr($fragment, 1) : $fragment;
474
475 3
        return $new;
476
    }
477
478
    /**
479
     * Return the string representation as a URI reference.
480
     *
481
     * Depending on which components of the URI are present, the resulting
482
     * string is either a full URI or relative reference according to RFC 3986,
483
     * Section 4.1. The method concatenates the various components of the URI,
484
     * using the appropriate delimiters:
485
     *
486
     * - If a scheme is present, it MUST be suffixed by ":".
487
     * - If an authority is present, it MUST be prefixed by "//".
488
     * - The path can be concatenated without delimiters. But there are two
489
     *   cases where the path has to be adjusted to make the URI reference
490
     *   valid as PHP does not allow to throw an exception in __toString():
491
     *     - If the path is rootless and an authority is present, the path MUST
492
     *       be prefixed by "/".
493
     *     - If the path is starting with more than one "/" and no authority is
494
     *       present, the starting slashes MUST be reduced to one.
495
     * - If a query is present, it MUST be prefixed by "?".
496
     * - If a fragment is present, it MUST be prefixed by "#".
497
     *
498
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
499
     *
500
     * @return string
501
     */
502 19
    public function __toString(): string
503
    {
504 19
        $scheme = $this->url['scheme'];
505 19
        $query = $this->url['query'];
506 19
        $fragment = $this->url['fragment'];
507
508 19
        return $this->createUriString(
509 19
            ($scheme !== '') ? $scheme.'://' : '',
510 19
               $this->getAuthority(),
511 19
               $this->getPath(),
512 19
               ($query !== '') ? '?'.$query : '',
513 19
               ($fragment !== '') ? '#'.$fragment : ''
514
        );
515
    }
516
}
517