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

Uri::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 10
cts 10
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 17
nc 1
nop 8
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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