Completed
Push — master ( ecf3d4...c56662 )
by Bohuslav
02:44
created

Uri::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 21
ccs 11
cts 11
cp 1
rs 9.3142
cc 1
eloc 18
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
// \Http\Message
11
use Kambo\Http\Message\Utils\UriValidator;
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\Http\Message
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
     * Instance of uri validator
127
     *
128
     * @var UriValidator
129
     */
130
    private $validator;
131
132
    /**
133
     * Create new Uri.
134
     *
135
     * @param string $scheme   Uri scheme.
136
     * @param string $host     Uri host.
137
     * @param int    $port     Uri port number.
138
     * @param string $path     Uri path.
139
     * @param string $query    Uri query string.
140
     * @param string $fragment Uri fragment.
141
     * @param string $user     Uri user.
142
     * @param string $password Uri password.
143
     */
144 73
    public function __construct(
145
        $scheme,
146
        $host,
147
        $port = null,
148
        $path = '/',
149
        $query = '',
150
        $fragment = '',
151
        $user = '',
152
        $password = ''
153
    ) {
154 73
        $this->scheme   = $this->normalizeScheme($scheme);
155 73
        $this->host     = strtolower($host);
156 73
        $this->port     = $port;
157 73
        $this->path     = $this->normalizePath($path);
158 72
        $this->query    = $this->urlEncode($query);
159 72
        $this->fragment = $fragment;
160 72
        $this->user     = $user;
161 72
        $this->password = $password;
162
163 72
        $this->validator = new UriValidator();
164 72
    }
165
166
    /**
167
     * Retrieve the scheme component of the URI.
168
     *
169
     * If no scheme is present, this method return an empty string.
170
     *
171
     * Underline implementation normalize schema to lowercase, as described in RFC 3986
172
     * Section 3.1.
173
     *
174
     * The trailing ":" character is not part of the scheme and is not added.
175
     *
176
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
177
     *
178
     * @return string The URI scheme.
179
     */
180 11
    public function getScheme()
181
    {
182 11
        return $this->scheme;
183
    }
184
185
    /**
186
     * Retrieve the authority component of the URI.
187
     *
188
     * If no authority information is present, this method returns an empty
189
     * string.
190
     *
191
     * The authority syntax of the URI is:
192
     *
193
     * <pre>
194
     * [user-info@]host[:port]
195
     * </pre>
196
     *
197
     * If the port component is not set or is the standard port for the current
198
     * scheme, it is not included.
199
     *
200
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
201
     *
202
     * @return string The URI authority, in "[user-info@]host[:port]" format.
203
     */
204 7
    public function getAuthority()
205
    {
206 7
        $port     = $this->getPort();
207 7
        $userInfo = $this->getUserInfo();
208
209 7
        return ($userInfo ? $userInfo . '@' : '') . $this->getHost() . ($port ? ':' . $port : '');
210
    }
211
212
    /**
213
     * Retrieve the user information component of the URI.
214
     *
215
     * If no user information is present, this method returns an empty string.
216
     *
217
     * If a user is present in the URI, this method return that value;
218
     * additionally, if the password is also present, it will be appended to the
219
     * user value, with a colon (":") separating the values.
220
     *
221
     * The trailing "@" character is not part of the user information and is not added.
222
     *
223
     * @return string The URI user information, in "username[:password]" format.
224
     */
225 13
    public function getUserInfo()
226
    {
227 13
        $userInfo = '';
228 13
        if (!empty($this->user)) {
229 7
            $userInfo = $this->user;
230 7
            if (!empty($this->password)) {
231 6
                $userInfo .= ':' . $this->password;
232 6
            }
233 7
        }
234
235 13
        return $userInfo;
236
    }
237
238
    /**
239
     * Retrieve the host component of the URI.
240
     *
241
     * If no host is present, this method returns an empty string.
242
     *
243
     * Underline implementation normalize the host name to lowercase, as described in RFC 3986
244
     * Section 3.2.2.
245
     *
246
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
247
     *
248
     * @return string The URI host.
249
     */
250 21
    public function getHost()
251
    {
252 21
        return $this->host;
253
    }
254
255
    /**
256
     * Retrieve the port component of the URI.
257
     *
258
     * If a port is present, and it is non-standard for the current scheme,
259
     * this method return it as an integer. If the port is the standard port
260
     * used with the current scheme, this method return null.
261
     *
262
     * If no port is present, and no scheme is present, this method returns
263
     * a null value.
264
     *
265
     * If no port is present, but a scheme is present, this method MAY return
266
     * the standard port for that scheme, but SHOULD return null.
267
     *
268
     * @return null|int The URI port.
269
     */
270 13
    public function getPort()
271
    {
272 13
        return $this->isStandardPort() ? null : $this->port;
273
    }
274
275
    /**
276
     * Retrieve the path component of the URI.
277
     *
278
     * The path can either be empty or absolute (starting with a slash) or
279
     * rootless (not starting with a slash). Implementation support all
280
     * three syntaxes.
281
     *
282
     * Normally, the empty path "" and absolute path "/" are considered equal as
283
     * defined in RFC 7230 Section 2.7.3. But this method does not automatically
284
     * do this normalization because in contexts with a trimmed base path, e.g.
285
     * the front controller, this difference becomes significant. It's the task
286
     * of the user to handle both "" and "/".
287
     *
288
     * The value returned is percent-encoded, and underline implementation ensure that 
289
     * the value is not double-encode. Encoded characters are defined in 
290
     * RFC 3986, Sections 2 and 3.4.
291
     *
292
     * As an example, if the value should include a slash ("/") not intended as
293
     * delimiter between path segments, that value is passed in encoded
294
     * form (e.g., "%2F") to the instance.
295
     *
296
     * @see https://tools.ietf.org/html/rfc3986#section-2
297
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
298
     *
299
     * @return string The URI path.
300
     */
301 19
    public function getPath()
302
    {
303 19
        return $this->path;
304
    }
305
306
    /**
307
     * Retrieve the query string of the URI.
308
     *
309
     * If no query string is present, this method returns an empty string.
310
     *
311
     * The leading "?" character is not part of the query and it is not added.
312
     *
313
     * The value returned is percent-encoded, and implementation ensure that 
314
     * the value is not double-encode. Encoded characters are defined in 
315
     * RFC 3986, Sections 2 and 3.4.
316
     *
317
     * As an example, if a value in a key/value pair of the query string should
318
     * include an ampersand ("&") not intended as a delimiter between values,
319
     * that value is passed in encoded form (e.g., "%26") to the instance.
320
     *
321
     * @see https://tools.ietf.org/html/rfc3986#section-2
322
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
323
     *
324
     * @return string The URI query string.
325
     */
326 20
    public function getQuery()
327
    {
328 20
        return $this->query;
329
    }
330
331
332
    /**
333
     * Retrieve the fragment component of the URI.
334
     *
335
     * If no fragment is present, this method returns an empty string.
336
     *
337
     * The leading "#" character is not part of the fragment and it is not
338
     * added.
339
     *
340
     * The value returned is percent-encoded, and underline implementation ensure that 
341
     * the value is not double-encode. Encoded characters are defined in 
342
     * RFC 3986, Sections 2 and 3.4.
343
     *
344
     * @see https://tools.ietf.org/html/rfc3986#section-2
345
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
346
     *
347
     * @return string The URI fragment.
348
     */
349 11
    public function getFragment()
350
    {
351 11
        return $this->fragment;
352
    }
353
354
    /**
355
     * Return an instance with the specified scheme.
356
     *
357
     * This method retains the state of the current instance, and returns
358
     * an instance that contains the specified scheme.
359
     *
360
     * Implementations support the schemes "http" and "https" case
361
     * insensitively.
362
     *
363
     * An empty scheme is equivalent to removing the scheme.
364
     *
365
     * @param string $scheme The scheme to use with the new instance.
366
     *
367
     * @return self A new instance with the specified scheme.
368
     *
369
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
370
     */
371 3
    public function withScheme($scheme)
372
    {
373 3
        $clone         = clone $this;
374 3
        $clone->scheme = $this->normalizeScheme($scheme);
375
376 1
        return $clone;
377
    }
378
379
    /**
380
     * Return an instance with the specified user information.
381
     *
382
     * This method retains the state of the current instance, and returns
383
     * an instance that contains the specified user information.
384
     *
385
     * Password is optional, but the user information include the
386
     * user; an empty string for the user is equivalent to removing user
387
     * information.
388
     *
389
     * @param string      $user     The user name to use for authority.
390
     * @param null|string $password The password associated with $user.
391
     *
392
     * @return self A new instance with the specified user information.
393
     */
394 2
    public function withUserInfo($user, $password = null)
395
    {
396 2
        $clone           = clone $this;
397 2
        $clone->user     = $user;
398 2
        $clone->password = $password;
399
400 2
        return $clone;
401
    }
402
403
    /**
404
     * Return an instance with the specified host.
405
     *
406
     * This method retains the state of the current instance, and returns
407
     * an instance that contains the specified host.
408
     *
409
     * An empty host value is equivalent to removing the host.
410
     *
411
     * @param string $host The hostname to use with the new instance.
412
     *
413
     * @return self A new instance with the specified host.
414
     *
415
     * @throws \InvalidArgumentException for invalid hostnames.
416
     */
417 1
    public function withHost($host)
418
    {
419 1
        $clone       = clone $this;
420 1
        $clone->host = strtolower($host);
421
422 1
        return $clone;
423
    }
424
425
    /**
426
     * Return an instance with the specified port.
427
     *
428
     * This method retains the state of the current instance, and return
429
     * an instance that contains the specified port.
430
     *
431
     * Method raise an exception for ports outside the
432
     * established TCP and UDP port ranges.
433
     *
434
     * A null value provided for the port is equivalent to removing the port
435
     * information.
436
     *
437
     * @param null|int $port The port to use with the new instance; a null value
438
     *                       removes the port information.
439
     *
440
     * @return self A new instance with the specified port.
441
     *
442
     * @throws \InvalidArgumentException for invalid ports.
443
     */
444 2
    public function withPort($port)
445
    {
446 2
        $this->validator->validatePort($port);
447
448 1
        $clone       = clone $this;
449 1
        $clone->port = $port;
450
451 1
        return $clone;
452
    }
453
454
    /**
455
     * Return an instance with the specified path.
456
     *
457
     * This method retains the state of the current instance, and return
458
     * an instance that contains the specified path.
459
     *
460
     * The path can either be empty or absolute (starting with a slash) or
461
     * rootless (not starting with a slash). Implementations support all
462
     * three syntaxes.
463
     *
464
     * If the path is intended to be domain-relative rather than path relative then
465
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
466
     * are assumed to be relative to some base path known to the application or
467
     * consumer.
468
     *
469
     * Users can provide both encoded and decoded path characters.
470
     * Implementations ensure the correct encoding as outlined in getPath().
471
     *
472
     * @param string $path The path to use with the new instance.
473
     *
474
     * @return self A new instance with the specified path.
475
     *
476
     * @throws \InvalidArgumentException for invalid paths.
477
     */
478 4
    public function withPath($path)
479
    {
480 4
        $this->validator->validatePath($path);
481
482 1
        $clone       = clone $this;
483 1
        $clone->path = $this->urlEncode((string) $path);
484
485 1
        return $clone;
486
    }
487
488
    /**
489
     * Return an instance with the specified query string.
490
     *
491
     * This method retains the state of the current instance, and return
492
     * an instance that contains the specified query string.
493
     *
494
     * Users can provide both encoded and decoded query characters.
495
     * Implementation ensure the correct encoding as outlined in getQuery().
496
     *
497
     * An empty query string value is equivalent to removing the query string.
498
     *
499
     * @param string $query The query string to use with the new instance.
500
     *
501
     * @return self A new instance with the specified query string.
502
     *
503
     * @throws \InvalidArgumentException for invalid query strings.
504
     */
505 5
    public function withQuery($query)
506
    {
507 5
        $this->validator->validateQuery($query);
508
509 2
        $clone        = clone $this;
510 2
        $clone->query = $this->urlEncode((string) $query);
511
512 2
        return $clone;
513
    }
514
515
    /**
516
     * Return an instance with the specified URI fragment.
517
     *
518
     * This method retains the state of the current instance, and return
519
     * an instance that contains the specified URI fragment.
520
     *
521
     * Users can provide both encoded and decoded fragment characters.
522
     * Implementation ensure the correct encoding as outlined in getFragment().
523
     *
524
     * An empty fragment value is equivalent to removing the fragment.
525
     *
526
     * @param string $fragment The fragment to use with the new instance.
527
     *
528
     * @return self A new instance with the specified fragment.
529
     */
530 1
    public function withFragment($fragment)
531
    {
532 1
        $clone           = clone $this;
533 1
        $clone->fragment = $fragment;
534
535 1
        return $clone;
536
    }
537
538
    /**
539
     * Return the string representation as a URI reference.
540
     *
541
     * Depending on which components of the URI are present, the resulting
542
     * string is either a full URI or relative reference according to RFC 3986,
543
     * Section 4.1. The method concatenates the various components of the URI,
544
     * using the appropriate delimiters:
545
     *
546
     * - If a scheme is present, it is suffixed by ":".
547
     * - If an authority is present, it is prefixed by "//".
548
     * - The path can be concatenated without delimiters. But there are two
549
     *   cases where the path has to be adjusted to make the URI reference
550
     *   valid as PHP does not allow to throw an exception in __toString():
551
     *     - If the path is rootless and an authority is present, the path is
552
     *       be prefixed by "/".
553
     *     - If the path is starting with more than one "/" and no authority is
554
     *       present, the starting slashes are be reduced to one.
555
     * - If a query is present, it is prefixed by "?".
556
     * - If a fragment is present, it is prefixed by "#".
557
     *
558
     *         foo://example.com:8042/over/there?name=ferret#nose
559
     *         \_/   \______________/\_________/ \_________/ \__/
560
     *          |           |            |            |        |
561
     *       scheme     authority       path        query   fragment   
562
     *  
563
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
564
     *
565
     * @return string
566
     */
567 7
    public function __toString()
568
    {
569 7
        $scheme    = $this->getScheme();
570 7
        $authority = $this->getAuthority();
571 7
        $path      = $this->getPath();
572 7
        $query     = $this->getQuery();
573 7
        $fragment  = $this->getFragment();
574
575 7
        $path = '/' . ltrim($path, '/');
576
577 7
        return ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : '')
578 7
            . $path . ($query ? '?' . $query : '') . ($fragment ? '#' . $fragment : '');
579
    }
580
581
    // ------------ PRIVATE METHODS
582
583
    /**
584
     * Check if Uri use a standard port.
585
     *
586
     * @return bool
587
     */
588 13
    private function isStandardPort()
589
    {
590 13
        return ($this->scheme === self::HTTP && $this->port === self::HTTP_PORT)
591 13
            || ($this->scheme === self::HTTPS && $this->port === self::HTTPS_PORT);
592
    }
593
594
    /**
595
     * Normalize scheme part of Uri.
596
     *
597
     * @param string $scheme Raw Uri scheme.
598
     *
599
     * @return string Normalized Uri
600
     *
601
     * @throws InvalidArgumentException If the Uri scheme is not a string.
602
     * @throws InvalidArgumentException If Uri scheme is not "", "https", or "http".
603
     */
604 73
    private function normalizeScheme($scheme)
605
    {
606 73
        if (!is_string($scheme) && !method_exists($scheme, '__toString')) {
607 1
            throw new InvalidArgumentException('Uri scheme must be a string');
608
        }
609
610 73
        $scheme = str_replace('://', '', strtolower((string) $scheme));
611 73
        if (!isset($this->validSchema[$scheme])) {
612 1
            throw new InvalidArgumentException('Uri scheme must be one of: "", "https", "http"');
613
        }
614
615 73
        return $scheme;
616
    }
617
618
    /**
619
     * Normalize path part of Uri and ensure it is properly encoded..
620
     *
621
     * @param string $path Raw Uri path.
622
     *
623
     * @return string Normalized Uri path
624
     *
625
     * @throws InvalidArgumentException If the Uri scheme is not a string.
626
     */
627 73
    private function normalizePath($path)
628
    {
629 73
        if (!is_string($path) && !method_exists($path, '__toString')) {
630 1
            throw new InvalidArgumentException('Uri path must be a string');
631
        }
632
633 72
        $path = $this->urlEncode($path);
634
635 72
        if (empty($path)) {
636 1
            return '';
637
        }
638
639 71
        if ($path[0] !== '/') {
640 1
            return $path;
641
        }
642
643
        // Ensure only one leading slash
644 70
        return '/' . ltrim($path, '/');
645
    }
646
647
    /**
648
     * Url encode 
649
     *
650
     * This method percent-encodes all reserved
651
     * characters in the provided path string. This method
652
     * will NOT double-encode characters that are already
653
     * percent-encoded.
654
     *
655
     * @param  string $path The raw uri path.
656
     *
657
     * @return string The RFC 3986 percent-encoded uri path.
658
     *
659
     * @link   http://www.faqs.org/rfcs/rfc3986.html
660
     */
661 72
    private function urlEncode($path)
662
    {
663 72
        return preg_replace_callback(
664 72
            '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
665 72
            function ($match) {
666 1
                return rawurlencode($match[0]);
667 72
            },
668
            $path
669 72
        );
670
    }
671
}
672