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