Completed
Push — master ( 4fd434...e1a522 )
by Bohuslav
03:35
created

Uri::withHost()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
crap 1
1
<?php
2
namespace Kambo\Http\Message;
3
4
// \Spl
5
use InvalidArgumentException;
6
7
// \Psr
8
use Psr\Http\Message\UriInterface;
9
10
/**
11
 * Value object representing a URI.
12
 *
13
 * This class represent URIs according to RFC 3986 and to
14
 * provide methods for most common operations. Additional functionality for
15
 * working with URIs can be provided on top of the object or externally.
16
 * Its primary use is for HTTP requests, but may also be used in other
17
 * contexts.
18
 *
19
 * Instances of the uri are considered immutable; all methods that
20
 * change state retain the internal state of the current message and return
21
 * an instance that contains the changed state.
22
 *
23
 * @link http://tools.ietf.org/html/rfc3986 (the URI specification)
24
 *
25
 * @package Kambo\Http\Message
26
 * @author  Bohuslav Simek <[email protected]>
27
 * @license MIT
28
 */
29
class Uri implements UriInterface
30
{
31
    const HTTP  = 'http';
32
    const HTTPS = 'https';
33
34
    /**
35
     * Http port
36
     *
37
     * @var integer
38
     */
39
    const HTTP_PORT = 80;
40
41
    /**
42
     * Https port
43
     *
44
     * @var integer
45
     */
46
    const HTTPS_PORT = 443;
47
48
    /**
49
     * Valid URI schemas
50
     * Schema can be http, https or empty.
51
     *
52
     * @var array in format [ string "schema"=> boolean true (true valid), ... ]
53
     */
54
    private $validSchema = [
55
        '' => true,
56
        self::HTTPS => true,
57
        self::HTTP => true,
58
    ];
59
60
    /**
61
     * URL scheme of the URI.
62
     * Default value is an empty string.
63
     *
64
     * @var string
65
     */
66
    private $scheme = '';
67
68
    /**
69
     * Path component of the URI.
70
     * Default value is an empty string.
71
     *
72
     * @var string
73
     */
74
    private $path = '';
75
76
    /**
77
     * Query string of the URI.
78
     * Default value is an empty string.
79
     *
80
     * @var string
81
     */
82
    private $query = '';
83
84
    /**
85
     * Fragment component of the URI.
86
     * Default value is an empty string.
87
     *
88
     * @var string
89
     */
90
    private $fragment = '';
91
92
    /**
93
     * User part of the URI.
94
     * Default value is an empty string.
95
     *
96
     * @var string
97
     */
98
    private $user = '';
99
100
    /**
101
     * Password part of the URI.
102
     * Default value is an empty string.
103
     *
104
     * @var string
105
     */
106
    private $password = '';
107
108
    /**
109
     * Host component of the URI eg. foo.bar
110
     *
111
     * @var string
112
     */
113
    private $host;
114
115
    /**
116
     * Port component of the URI eg. 443.
117
     *
118
     * @var integer
119
     */
120
    private $port;
121
122
    /**
123
     * Create new Uri.
124
     *
125
     * @param string $scheme   Uri scheme.
126
     * @param string $host     Uri host.
127
     * @param int    $port     Uri port number.
128
     * @param string $path     Uri path.
129
     * @param string $query    Uri query string.
130
     * @param string $fragment Uri fragment.
131
     * @param string $user     Uri user.
132
     * @param string $password Uri password.
133
     */
134 71
    public function __construct(
135
        $scheme,
136
        $host,
137
        $port = null,
138
        $path = '/',
139
        $query = '',
140
        $fragment = '',
141
        $user = '',
142
        $password = ''
143
    ) {
144 71
        $this->scheme   = $this->normalizeScheme($scheme);
145 71
        $this->host     = strtolower($host);
146 71
        $this->port     = $port;
147 71
        $this->path     = $this->normalizePath($path);
148 70
        $this->query    = $this->urlEncode($query);
149 70
        $this->fragment = $fragment;
150 70
        $this->user     = $user;
151 70
        $this->password = $password;
152 70
    }
153
154
    /**
155
     * Retrieve the scheme component of the URI.
156
     *
157
     * If no scheme is present, this method return an empty string.
158
     *
159
     * Underline implementation normalize schema to lowercase, as described in RFC 3986
160
     * Section 3.1.
161
     *
162
     * The trailing ":" character is not part of the scheme and is not added.
163
     *
164
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
165
     *
166
     * @return string The URI scheme.
167
     */
168 11
    public function getScheme()
169
    {
170 11
        return $this->scheme;
171
    }
172
173
    /**
174
     * Retrieve the authority component of the URI.
175
     *
176
     * If no authority information is present, this method returns an empty
177
     * string.
178
     *
179
     * The authority syntax of the URI is:
180
     *
181
     * <pre>
182
     * [user-info@]host[:port]
183
     * </pre>
184
     *
185
     * If the port component is not set or is the standard port for the current
186
     * scheme, it is not included.
187
     *
188
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
189
     *
190
     * @return string The URI authority, in "[user-info@]host[:port]" format.
191
     */
192 7
    public function getAuthority()
193
    {
194 7
        $port     = $this->getPort();
195 7
        $userInfo = $this->getUserInfo();
196
197 7
        return ($userInfo ? $userInfo . '@' : '') . $this->getHost() . ($port ? ':' . $port : '');
198
    }
199
200
    /**
201
     * Retrieve the user information component of the URI.
202
     *
203
     * If no user information is present, this method returns an empty string.
204
     *
205
     * If a user is present in the URI, this method return that value;
206
     * additionally, if the password is also present, it will be appended to the
207
     * user value, with a colon (":") separating the values.
208
     *
209
     * The trailing "@" character is not part of the user information and is not added.
210
     *
211
     * @return string The URI user information, in "username[:password]" format.
212
     */
213 13
    public function getUserInfo()
214
    {
215 13
        $userInfo = '';
216 13
        if (!empty($this->user)) {
217 7
            $userInfo = $this->user;
218 7
            if (!empty($this->password)) {
219 6
                $userInfo .= ':' . $this->password;
220 6
            }
221 7
        }
222
223 13
        return $userInfo;
224
    }
225
226
    /**
227
     * Retrieve the host component of the URI.
228
     *
229
     * If no host is present, this method returns an empty string.
230
     *
231
     * Underline implementation normalize the host name to lowercase, as described in RFC 3986
232
     * Section 3.2.2.
233
     *
234
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
235
     *
236
     * @return string The URI host.
237
     */
238 21
    public function getHost()
239
    {
240 21
        return $this->host;
241
    }
242
243
    /**
244
     * Retrieve the port component of the URI.
245
     *
246
     * If a port is present, and it is non-standard for the current scheme,
247
     * this method return it as an integer. If the port is the standard port
248
     * used with the current scheme, this method return null.
249
     *
250
     * If no port is present, and no scheme is present, this method returns
251
     * a null value.
252
     *
253
     * If no port is present, but a scheme is present, this method MAY return
254
     * the standard port for that scheme, but SHOULD return null.
255
     *
256
     * @return null|int The URI port.
257
     */
258 13
    public function getPort()
259
    {
260 13
        return $this->isStandardPort() ? null : $this->port;
261
    }
262
263
    /**
264
     * Retrieve the path component of the URI.
265
     *
266
     * The path can either be empty or absolute (starting with a slash) or
267
     * rootless (not starting with a slash). Implementation support all
268
     * three syntaxes.
269
     *
270
     * Normally, the empty path "" and absolute path "/" are considered equal as
271
     * defined in RFC 7230 Section 2.7.3. But this method does not automatically
272
     * do this normalization because in contexts with a trimmed base path, e.g.
273
     * the front controller, this difference becomes significant. It's the task
274
     * of the user to handle both "" and "/".
275
     *
276
     * The value returned is percent-encoded, and underline implementation ensure that 
277
     * the value is not double-encode. Encoded characters are defined in 
278
     * RFC 3986, Sections 2 and 3.4.
279
     *
280
     * As an example, if the value should include a slash ("/") not intended as
281
     * delimiter between path segments, that value is passed in encoded
282
     * form (e.g., "%2F") to the instance.
283
     *
284
     * @see https://tools.ietf.org/html/rfc3986#section-2
285
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
286
     *
287
     * @return string The URI path.
288
     */
289 19
    public function getPath()
290
    {
291 19
        return $this->path;
292
    }
293
294
    /**
295
     * Retrieve the query string of the URI.
296
     *
297
     * If no query string is present, this method returns an empty string.
298
     *
299
     * The leading "?" character is not part of the query and it is not added.
300
     *
301
     * The value returned is percent-encoded, and implementation ensure that 
302
     * the value is not double-encode. Encoded characters are defined in 
303
     * RFC 3986, Sections 2 and 3.4.
304
     *
305
     * As an example, if a value in a key/value pair of the query string should
306
     * include an ampersand ("&") not intended as a delimiter between values,
307
     * that value is passed in encoded form (e.g., "%26") to the instance.
308
     *
309
     * @see https://tools.ietf.org/html/rfc3986#section-2
310
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
311
     *
312
     * @return string The URI query string.
313
     */
314 20
    public function getQuery()
315
    {
316 20
        return $this->query;
317
    }
318
319
320
    /**
321
     * Retrieve the fragment component of the URI.
322
     *
323
     * If no fragment is present, this method returns an empty string.
324
     *
325
     * The leading "#" character is not part of the fragment and it is not
326
     * added.
327
     *
328
     * The value returned is percent-encoded, and underline implementation ensure that 
329
     * the value is not double-encode. Encoded characters are defined in 
330
     * RFC 3986, Sections 2 and 3.4.
331
     *
332
     * @see https://tools.ietf.org/html/rfc3986#section-2
333
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
334
     *
335
     * @return string The URI fragment.
336
     */
337 11
    public function getFragment()
338
    {
339 11
        return $this->fragment;
340
    }
341
342
    /**
343
     * Return an instance with the specified scheme.
344
     *
345
     * This method retains the state of the current instance, and returns
346
     * an instance that contains the specified scheme.
347
     *
348
     * Implementations support the schemes "http" and "https" case
349
     * insensitively.
350
     *
351
     * An empty scheme is equivalent to removing the scheme.
352
     *
353
     * @param string $scheme The scheme to use with the new instance.
354
     *
355
     * @return self A new instance with the specified scheme.
356
     *
357
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
358
     */
359 3
    public function withScheme($scheme)
360
    {
361 3
        $clone         = clone $this;
362 3
        $clone->scheme = $this->normalizeScheme($scheme);
363
364 1
        return $clone;
365
    }
366
367
    /**
368
     * Return an instance with the specified user information.
369
     *
370
     * This method retains the state of the current instance, and returns
371
     * an instance that contains the specified user information.
372
     *
373
     * Password is optional, but the user information include the
374
     * user; an empty string for the user is equivalent to removing user
375
     * information.
376
     *
377
     * @param string $user The user name to use for authority.
378
     * @param null|string $password The password associated with $user.
379
     *
380
     * @return self A new instance with the specified user information.
381
     */
382 2
    public function withUserInfo($user, $password = null)
383
    {
384 2
        $clone           = clone $this;
385 2
        $clone->user     = $user;
386 2
        $clone->password = $password;
387
388 2
        return $clone;
389
    }
390
391
    /**
392
     * Return an instance with the specified host.
393
     *
394
     * This method retains the state of the current instance, and returns
395
     * an instance that contains the specified host.
396
     *
397
     * An empty host value is equivalent to removing the host.
398
     *
399
     * @param string $host The hostname to use with the new instance.
400
     *
401
     * @return self A new instance with the specified host.
402
     *
403
     * @throws \InvalidArgumentException for invalid hostnames.
404
     */
405 1
    public function withHost($host)
406
    {
407 1
        $clone       = clone $this;
408 1
        $clone->host = strtolower($host);
409
410 1
        return $clone;
411
    }
412
413
    /**
414
     * Return an instance with the specified port.
415
     *
416
     * This method retains the state of the current instance, and return
417
     * an instance that contains the specified port.
418
     *
419
     * Method raise an exception for ports outside the
420
     * established TCP and UDP port ranges.
421
     *
422
     * A null value provided for the port is equivalent to removing the port
423
     * information.
424
     *
425
     * @param null|int $port The port to use with the new instance; a null value
426
     *     removes the port information.
427
     *
428
     * @return self A new instance with the specified port.
429
     *
430
     * @throws \InvalidArgumentException for invalid ports.
431
     */
432 2
    public function withPort($port)
433
    {
434 2
        $clone       = clone $this;
435 2
        $clone->port = $this->validatePort($port);
436
437 1
        return $clone;
438
    }
439
440
    /**
441
     * Return an instance with the specified path.
442
     *
443
     * This method retains the state of the current instance, and return
444
     * an instance that contains the specified path.
445
     *
446
     * The path can either be empty or absolute (starting with a slash) or
447
     * rootless (not starting with a slash). Implementations support all
448
     * three syntaxes.
449
     *
450
     * If the path is intended to be domain-relative rather than path relative then
451
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
452
     * are assumed to be relative to some base path known to the application or
453
     * consumer.
454
     *
455
     * Users can provide both encoded and decoded path characters.
456
     * Implementations ensure the correct encoding as outlined in getPath().
457
     *
458
     * @param string $path The path to use with the new instance.
459
     *
460
     * @return self A new instance with the specified path.
461
     *
462
     * @throws \InvalidArgumentException for invalid paths.
463
     */
464 4
    public function withPath($path)
465
    {
466 4
        $this->validatePath($path);
467
468 1
        $clone       = clone $this;
469 1
        $clone->path = $this->urlEncode((string) $path);
470
471 1
        return $clone;
472
    }
473
474
    /**
475
     * Return an instance with the specified query string.
476
     *
477
     * This method retains the state of the current instance, and return
478
     * an instance that contains the specified query string.
479
     *
480
     * Users can provide both encoded and decoded query characters.
481
     * Implementation ensure the correct encoding as outlined in getQuery().
482
     *
483
     * An empty query string value is equivalent to removing the query string.
484
     *
485
     * @param string $query The query string to use with the new instance.
486
     *
487
     * @return self A new instance with the specified query string.
488
     *
489
     * @throws \InvalidArgumentException for invalid query strings.
490
     */
491 5
    public function withQuery($query)
492
    {
493 5
        $this->validateQuery($query);
494
495 2
        $clone        = clone $this;
496 2
        $clone->query = $this->urlEncode((string) $query);
497
498 2
        return $clone;
499
    }
500
501
    /**
502
     * Return an instance with the specified URI fragment.
503
     *
504
     * This method retains the state of the current instance, and return
505
     * an instance that contains the specified URI fragment.
506
     *
507
     * Users can provide both encoded and decoded fragment characters.
508
     * Implementation ensure the correct encoding as outlined in getFragment().
509
     *
510
     * An empty fragment value is equivalent to removing the fragment.
511
     *
512
     * @param string $fragment The fragment to use with the new instance.
513
     *
514
     * @return self A new instance with the specified fragment.
515
     */
516 1
    public function withFragment($fragment)
517
    {
518 1
        $clone           = clone $this;
519 1
        $clone->fragment = $fragment;
520
521 1
        return $clone;
522
    }
523
524
    /**
525
     * Return the string representation as a URI reference.
526
     *
527
     * Depending on which components of the URI are present, the resulting
528
     * string is either a full URI or relative reference according to RFC 3986,
529
     * Section 4.1. The method concatenates the various components of the URI,
530
     * using the appropriate delimiters:
531
     *
532
     * - If a scheme is present, it is suffixed by ":".
533
     * - If an authority is present, it is prefixed by "//".
534
     * - The path can be concatenated without delimiters. But there are two
535
     *   cases where the path has to be adjusted to make the URI reference
536
     *   valid as PHP does not allow to throw an exception in __toString():
537
     *     - If the path is rootless and an authority is present, the path is
538
     *       be prefixed by "/".
539
     *     - If the path is starting with more than one "/" and no authority is
540
     *       present, the starting slashes are be reduced to one.
541
     * - If a query is present, it is prefixed by "?".
542
     * - If a fragment is present, it is prefixed by "#".
543
     *
544
     *         foo://example.com:8042/over/there?name=ferret#nose
545
     *         \_/   \______________/\_________/ \_________/ \__/
546
     *          |           |            |            |        |
547
     *       scheme     authority       path        query   fragment   
548
     *  
549
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
550
     *
551
     * @return string
552
     */
553 7
    public function __toString()
554
    {
555 7
        $scheme    = $this->getScheme();
556 7
        $authority = $this->getAuthority();
557 7
        $path      = $this->getPath();
558 7
        $query     = $this->getQuery();
559 7
        $fragment  = $this->getFragment();
560
561 7
        $path = '/' . ltrim($path, '/');
562
563 7
        return ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : '')
564 7
            . $path . ($query ? '?' . $query : '') . ($fragment ? '#' . $fragment : '');
565
    }
566
567
    // ------------ PRIVATE METHODS
568
569
    /**
570
     * Check if Uri use a standard port.
571
     *
572
     * @return bool
573
     */
574 13
    private function isStandardPort()
575
    {
576 13
        return ($this->scheme === self::HTTP && $this->port === self::HTTP_PORT)
577 13
            || ($this->scheme === self::HTTPS && $this->port === self::HTTPS_PORT);
578
    }
579
580
    /**
581
     * Normalize scheme part of Uri.
582
     *
583
     * @param string $scheme Raw Uri scheme.
584
     *
585
     * @return string Normalized Uri
586
     *
587
     * @throws InvalidArgumentException If the Uri scheme is not a string.
588
     * @throws InvalidArgumentException If Uri scheme is not "", "https", or "http".
589
     */
590 71
    private function normalizeScheme($scheme)
591
    {
592 71
        if (!is_string($scheme) && !method_exists($scheme, '__toString')) {
593 1
            throw new InvalidArgumentException('Uri scheme must be a string');
594
        }
595
596 71
        $scheme = str_replace('://', '', strtolower((string) $scheme));
597 71
        if (!isset($this->validSchema[$scheme])) {
598 1
            throw new InvalidArgumentException('Uri scheme must be one of: "", "https", "http"');
599
        }
600
601 71
        return $scheme;
602
    }
603
604
    /**
605
     * Normalize path part of Uri and ensure it is properly encoded..
606
     *
607
     * @param string $path Raw Uri path.
608
     *
609
     * @return string Normalized Uri path
610
     *
611
     * @throws InvalidArgumentException If the Uri scheme is not a string.
612
     */
613 71
    private function normalizePath($path)
614
    {
615 71
        if (!is_string($path) && !method_exists($path, '__toString')) {
616 1
            throw new InvalidArgumentException('Uri path must be a string');
617
        }
618
619 70
        $path = $this->urlEncode($path);
620
621 70
        if (empty($path)) {
622 1
            return '';
623
        }
624
625 69
        if ($path[0] !== '/') {
626 1
            return $path;
627
        }
628
629
        // Ensure only one leading slash
630 68
        return '/' . ltrim($path, '/');
631
    }
632
633
    /**
634
     * Validate Uri port.
635
     * Value can be null or integer between 1 and 65535.
636
     *
637
     * @param  null|int $port The Uri port number.
638
     *
639
     * @return null|int
640
     *
641
     * @throws InvalidArgumentException If the port is invalid.
642
     */
643 2
    private function validatePort($port)
644
    {
645 2
        if (is_null($port) || (is_integer($port) && ($port >= 1 && $port <= 65535))) {
646 1
            return $port;
647
        }
648
649 1
        throw new InvalidArgumentException('Uri port must be null or an integer between 1 and 65535 (inclusive)');
650
    }
651
652
    /**
653
     * Validate Uri path. 
654
     *
655
     * Path must NOT contain query string or URI fragment. It can be object,
656
     * but then the class must implement __toString method.
657
     *
658
     * @param  string|object $path The Uri path
659
     *
660
     * @return void
661
     *
662
     * @throws InvalidArgumentException If the path is invalid.
663
     */
664 4
    private function validatePath($path)
665
    {
666 4
        if (!is_string($path) && !method_exists($path, '__toString')) {
667 1
            throw new InvalidArgumentException(
668
                'Invalid path provided; must be a string'
669 1
            );
670
        }
671
672 3
        if (strpos($path, '?') !== false) {
673 1
            throw new InvalidArgumentException(
674
                'Invalid path provided; must not contain a query string'
675 1
            );
676
        }
677
678 2
        if (strpos($path, '#') !== false) {
679 1
            throw new InvalidArgumentException(
680
                'Invalid path provided; must not contain a URI fragment'
681 1
            );
682
        }
683 1
    }
684
685
    /**
686
     * Validate query. 
687
     *
688
     * Path must NOT contain URI fragment. It can be object,
689
     * but then the class must implement __toString method.
690
     *
691
     * @param  string|object $query The query path
692
     *
693
     * @return void
694
     *
695
     * @throws InvalidArgumentException If the query is invalid.
696
     */
697 5
    private function validateQuery($query)
698
    {
699 5
        if (!is_string($query) && !method_exists($query, '__toString')) {
700 2
            throw new InvalidArgumentException(
701
                'Query must be a string'
702 2
            );
703
        }
704
705 3
        if (strpos($query, '#') !== false) {
706 1
            throw new InvalidArgumentException(
707
                'Query must not contain a URI fragment'
708 1
            );
709
        }
710 2
    }
711
712
    /**
713
     * Url encode 
714
     *
715
     * This method percent-encodes all reserved
716
     * characters in the provided path string. This method
717
     * will NOT double-encode characters that are already
718
     * percent-encoded.
719
     *
720
     * @param  string $path The raw uri path.
721
     *
722
     * @return string The RFC 3986 percent-encoded uri path.
723
     *
724
     * @link   http://www.faqs.org/rfcs/rfc3986.html
725
     */
726 70
    private function urlEncode($path)
727
    {
728 70
        return preg_replace_callback(
729 70
            '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
730 70
            function ($match) {
731 1
                return rawurlencode($match[0]);
732 70
            },
733
            $path
734 70
        );
735
    }
736
}
737