Completed
Push — master ( 495dcc...46576c )
by ignace nyamagana
03:24
created

Uri::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

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

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
3
/**
4
 * League.Uri (https://uri.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Uri;
15
16
use finfo;
17
use League\Uri\Contract\UriInterface;
18
use League\Uri\Exception\IdnSupportMissing;
19
use League\Uri\Exception\SyntaxError;
20
use Psr\Http\Message\UriInterface as Psr7UriInterface;
21
use TypeError;
22
use function array_filter;
23
use function array_map;
24
use function base64_decode;
25
use function base64_encode;
26
use function count;
27
use function defined;
28
use function explode;
29
use function file_get_contents;
30
use function filter_var;
31
use function function_exists;
32
use function idn_to_ascii;
33
use function implode;
34
use function in_array;
35
use function inet_pton;
36
use function is_scalar;
37
use function mb_detect_encoding;
38
use function method_exists;
39
use function preg_match;
40
use function preg_replace;
41
use function preg_replace_callback;
42
use function rawurlencode;
43
use function sprintf;
44
use function str_replace;
45
use function strlen;
46
use function strpos;
47
use function strtolower;
48
use function substr;
49
use const FILEINFO_MIME;
50
use const FILTER_FLAG_IPV4;
51
use const FILTER_FLAG_IPV6;
52
use const FILTER_NULL_ON_FAILURE;
53
use const FILTER_VALIDATE_BOOLEAN;
54
use const FILTER_VALIDATE_IP;
55
use const IDNA_CHECK_BIDI;
56
use const IDNA_CHECK_CONTEXTJ;
57
use const IDNA_ERROR_BIDI;
58
use const IDNA_ERROR_CONTEXTJ;
59
use const IDNA_ERROR_DISALLOWED;
60
use const IDNA_ERROR_DOMAIN_NAME_TOO_LONG;
61
use const IDNA_ERROR_EMPTY_LABEL;
62
use const IDNA_ERROR_HYPHEN_3_4;
63
use const IDNA_ERROR_INVALID_ACE_LABEL;
64
use const IDNA_ERROR_LABEL_HAS_DOT;
65
use const IDNA_ERROR_LABEL_TOO_LONG;
66
use const IDNA_ERROR_LEADING_COMBINING_MARK;
67
use const IDNA_ERROR_LEADING_HYPHEN;
68
use const IDNA_ERROR_PUNYCODE;
69
use const IDNA_ERROR_TRAILING_HYPHEN;
70
use const IDNA_NONTRANSITIONAL_TO_ASCII;
71
use const IDNA_NONTRANSITIONAL_TO_UNICODE;
72
use const INTL_IDNA_VARIANT_UTS46;
73
74
final class Uri implements UriInterface
75
{
76
    /**
77
     * RFC3986 invalid characters.
78
     *
79
     * @see http://tools.ietf.org/html/rfc3986#section-2.2
80
     *
81
     * @var string
82
     */
83
    private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/';
84
85
    /**
86
     * RFC3986 Sub delimiter characters regular expression pattern.
87
     *
88
     * @see http://tools.ietf.org/html/rfc3986#section-2.2
89
     *
90
     * @var string
91
     */
92
    private const REGEXP_CHARS_SUBDELIM = "\!\$&'\(\)\*\+,;\=%";
93
94
    /**
95
     * RFC3986 unreserved characters regular expression pattern.
96
     *
97
     * @see http://tools.ietf.org/html/rfc3986#section-2.3
98
     *
99
     * @var string
100
     */
101
    private const REGEXP_CHARS_UNRESERVED = 'A-Za-z0-9_\-\.~';
102
103
104
    private const REGEXP_SCHEME = ',^[a-z]([-a-z0-9+.]+)?$,i';
105
106
    private const REGEXP_HOST_REGNAME = '/^(
107
        (?<unreserved>[a-z0-9_~\-\.])|
108
        (?<sub_delims>[!$&\'()*+,;=])|
109
        (?<encoded>%[A-F0-9]{2})
110
    )+$/x';
111
112
    private const REGEXP_HOST_GEN_DELIMS = '/[:\/?#\[\]@ ]/'; // Also includes space.
113
114
    private const REGEXP_HOST_IPFUTURE = '/^
115
        v(?<version>[A-F0-9])+\.
116
        (?:
117
            (?<unreserved>[a-z0-9_~\-\.])|
118
            (?<sub_delims>[!$&\'()*+,;=:])  # also include the : character
119
        )+
120
    $/ix';
121
122
    private const HOST_ADDRESS_BLOCK = "\xfe\x80";
123
124
    private const REGEXP_FILE_PATH = ',^(?<delim>/)?(?<root>[a-zA-Z][:|\|])(?<rest>.*)?,';
125
126
    private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
127
128
    private const REGEXP_BINARY = ',(;|^)base64$,';
129
130
    /**
131
     * IDNA errors.
132
     */
133
    private const IDNA_ERRORS = [
134
        IDNA_ERROR_EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty',
135
        IDNA_ERROR_LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes',
136
        IDNA_ERROR_DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form',
137
        IDNA_ERROR_LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")',
138
        IDNA_ERROR_TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")',
139
        IDNA_ERROR_HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions',
140
        IDNA_ERROR_LEADING_COMBINING_MARK => 'a label starts with a combining mark',
141
        IDNA_ERROR_DISALLOWED => 'a label or domain name contains disallowed characters',
142
        IDNA_ERROR_PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode',
143
        IDNA_ERROR_LABEL_HAS_DOT => 'a label contains a dot=full stop',
144
        IDNA_ERROR_INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string',
145
        IDNA_ERROR_BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)',
146
        IDNA_ERROR_CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements',
147
    ];
148
149
    private const REGEXP_WINDOW_PATH = ',^(?<root>[a-zA-Z][:|\|]),';
150
151
    /**
152
     * Supported schemes and corresponding default port.
153
     *
154
     * @var array
155
     */
156
    private const SCHEME_DEFAULT_PORT = [
157
        'data' => null,
158
        'file' => null,
159
        'ftp' => 21,
160
        'gopher' => 70,
161
        'http' => 80,
162
        'https' => 443,
163
        'ws' => 80,
164
        'wss' => 443,
165
    ];
166
167
    /**
168
     * URI validation methods per scheme.
169
     *
170
     * @var array
171
     */
172
    private const SCHEME_VALIDATION_METHOD = [
173
        'data' => 'isUriWithSchemeAndPathOnly',
174
        'file' => 'isUriWithSchemeHostAndPathOnly',
175
        'ftp' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
176
        'gopher' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
177
        'http' => 'isNonEmptyHostUri',
178
        'https' => 'isNonEmptyHostUri',
179
        'ws' => 'isNonEmptyHostUriWithoutFragment',
180
        'wss' => 'isNonEmptyHostUriWithoutFragment',
181
    ];
182
183
    /**
184
     * URI scheme component.
185
     *
186
     * @var string|null
187
     */
188
    private $scheme;
189
190
    /**
191
     * URI user info part.
192
     *
193
     * @var string|null
194
     */
195
    private $user_info;
196
197
    /**
198
     * URI host component.
199
     *
200
     * @var string|null
201
     */
202
    private $host;
203
204
    /**
205
     * URI port component.
206
     *
207
     * @var int|null
208
     */
209
    private $port;
210
211
    /**
212
     * URI authority string representation.
213
     *
214
     * @var string|null
215
     */
216
    private $authority;
217
218
    /**
219
     * URI path component.
220
     *
221
     * @var string
222
     */
223
    private $path = '';
224
225
    /**
226
     * URI query component.
227
     *
228
     * @var string|null
229
     */
230
    private $query;
231
232
    /**
233
     * URI fragment component.
234
     *
235
     * @var string|null
236
     */
237
    private $fragment;
238
239
    /**
240
     * URI string representation.
241
     *
242
     * @var string|null
243
     */
244
    private $uri;
245
246
    /**
247
     * Create a new instance.
248
     *
249
     * @param ?string $scheme
250
     * @param ?string $user
251
     * @param ?string $pass
252
     * @param ?string $host
253
     * @param ?int    $port
254
     * @param ?string $query
255
     * @param ?string $fragment
256
     */
257 300
    private function __construct(
258
        ?string $scheme,
259
        ?string $user,
260
        ?string $pass,
261
        ?string $host,
262
        ?int $port,
263
        string $path,
264
        ?string $query,
265
        ?string $fragment
266
    ) {
267 300
        $this->scheme = $this->formatScheme($scheme);
268 300
        $this->user_info = $this->formatUserInfo($user, $pass);
269 300
        $this->host = $this->formatHost($host);
270 300
        $this->port = $this->formatPort($port);
271 300
        $this->authority = $this->setAuthority();
272 300
        $this->path = $this->formatPath($path);
273 300
        $this->query = $this->formatQueryAndFragment($query);
274 300
        $this->fragment = $this->formatQueryAndFragment($fragment);
275 300
        $this->assertValidState();
276 286
    }
277
278
    /**
279
     * Format the Scheme and Host component.
280
     *
281
     * @param ?string $scheme
282
     *
283
     * @throws SyntaxError if the scheme is invalid
284
     */
285 306
    private function formatScheme(?string $scheme): ?string
286
    {
287 306
        if ('' === $scheme || null === $scheme) {
288 234
            return $scheme;
289
        }
290
291 238
        $formatted_scheme = strtolower($scheme);
292 238
        if (1 === preg_match(self::REGEXP_SCHEME, $formatted_scheme)) {
293 238
            return $formatted_scheme;
294
        }
295
296 2
        throw new SyntaxError(sprintf('The scheme `%s` is invalid', $scheme));
297
    }
298
299
    /**
300
     * Set the UserInfo component.
301
     *
302
     * @param ?string $user
303
     * @param ?string $password
304
     */
305 308
    private function formatUserInfo(?string $user, ?string $password): ?string
306
    {
307 308
        if (null === $user) {
308 282
            return $user;
309
        }
310
311 56
        static $user_pattern = '/(?:[^%'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
312 56
        $user = preg_replace_callback($user_pattern, [Uri::class, 'urlEncodeMatch'], $user);
313 56
        if (null === $password) {
314 6
            return $user;
315
        }
316
317 56
        static $password_pattern = '/(?:[^%:'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
318
319 56
        return $user.':'.preg_replace_callback($password_pattern, [Uri::class, 'urlEncodeMatch'], $password);
320
    }
321
322
    /**
323
     * Returns the RFC3986 encoded string matched.
324
     */
325 10
    private static function urlEncodeMatch(array $matches): string
326
    {
327 10
        return rawurlencode($matches[0]);
328
    }
329
330
    /**
331
     * Validate and Format the Host component.
332
     *
333
     * @param ?string $host
334
     */
335 332
    private function formatHost(?string $host): ?string
336
    {
337 332
        if (null === $host || '' === $host) {
338 234
            return $host;
339
        }
340
341 266
        if ('[' !== $host[0]) {
342 266
            return $this->formatRegisteredName($host);
343
        }
344
345 2
        return $this->formatIp($host);
346
    }
347
348
    /**
349
     * Validate and format a registered name.
350
     *
351
     * The host is converted to its ascii representation if needed
352
     *
353
     * @throws IdnSupportMissing if the submitted host required missing or misconfigured IDN support
354
     * @throws SyntaxError       if the submitted host is not a valid registered name
355
     */
356 264
    private function formatRegisteredName(string $host): string
357
    {
358
        // @codeCoverageIgnoreStart
359
        // added because it is not possible in travis to disabled the ext/intl extension
360
        // see travis issue https://github.com/travis-ci/travis-ci/issues/4701
361
        static $idn_support = null;
362
        $idn_support = $idn_support ?? function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46');
363
        // @codeCoverageIgnoreEnd
364
365 264
        $formatted_host = rawurldecode(strtolower($host));
366 264
        if (1 === preg_match(self::REGEXP_HOST_REGNAME, $formatted_host)) {
367 264
            if (false === strpos($formatted_host, 'xn--')) {
368 260
                return $formatted_host;
369
            }
370
371
            // @codeCoverageIgnoreStart
372
            if (!$idn_support) {
373
                throw new IdnSupportMissing(sprintf('the host `%s` could not be processed for IDN. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.', $host));
374
            }
375
            // @codeCoverageIgnoreEnd
376
377 8
            $unicode = idn_to_utf8(
378 8
                $host,
379 8
                IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_UNICODE,
380 8
                INTL_IDNA_VARIANT_UTS46,
381 4
                $arr
382
            );
383
384 8
            if (0 !== $arr['errors']) {
385 2
                throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors'])));
386
            }
387
388
            // @codeCoverageIgnoreStart
389
            if (false === $unicode) {
390
                throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
391
            }
392
            // @codeCoverageIgnoreEnd
393
394 6
            return $formatted_host;
395
        }
396
397 6
        if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, $formatted_host)) {
398 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : a registered name can not contain URI delimiters or spaces', $host));
399
        }
400
401
        // @codeCoverageIgnoreStart
402
        if (!$idn_support) {
403
            throw new IdnSupportMissing(sprintf('the host `%s` could not be processed for IDN. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.', $host));
404
        }
405
        // @codeCoverageIgnoreEnd
406
407 4
        $formatted_host = idn_to_ascii(
408 4
            $formatted_host,
409 4
            IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_ASCII,
410 4
            INTL_IDNA_VARIANT_UTS46,
411 4
            $arr
412
        );
413 4
        if (0 !== $arr['errors']) {
414 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors'])));
415
        }
416
417
        // @codeCoverageIgnoreStart
418
        if (false === $formatted_host) {
419
            throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
420
        }
421
        // @codeCoverageIgnoreEnd
422
423 2
        return $arr['result'];
424
    }
425
426
    /**
427
     * Retrieves and format IDNA conversion error message.
428
     *
429
     * @see http://icu-project.org/apiref/icu4j/com/ibm/icu/text/IDNA.Error.html
430
     */
431 4
    private function getIDNAErrors(int $error_byte): string
432
    {
433 4
        $res = [];
434 4
        foreach (self::IDNA_ERRORS as $error => $reason) {
435 4
            if ($error === ($error_byte & $error)) {
436 4
                $res[] = $reason;
437
            }
438
        }
439
440 4
        return [] === $res ? 'Unknown IDNA conversion error.' : implode(', ', $res).'.';
441
    }
442
443
    /**
444
     * Validate and Format the IPv6/IPvfuture host.
445
     *
446
     * @throws SyntaxError if the submitted host is not a valid IP host
447
     */
448 16
    private function formatIp(string $host): string
449
    {
450 16
        $ip = substr($host, 1, -1);
451 16
        if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
452 2
            return $host;
453
        }
454
455 14
        if (1 === preg_match(self::REGEXP_HOST_IPFUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) {
456 2
            return $host;
457
        }
458
459 12
        $pos = strpos($ip, '%');
460 12
        if (false === $pos) {
461 4
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
462
        }
463
464 8
        if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, rawurldecode(substr($ip, $pos)))) {
465 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
466
        }
467
468 6
        $ip = substr($ip, 0, $pos);
469 6
        if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
470 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
471
        }
472
473
        //Only the address block fe80::/10 can have a Zone ID attach to
474
        //let's detect the link local significant 10 bits
475 4
        if (0 === strpos((string) inet_pton($ip), self::HOST_ADDRESS_BLOCK)) {
476 2
            return $host;
477
        }
478
479 2
        throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
480
    }
481
482
    /**
483
     * Format the Port component.
484
     *
485
     * @param null|mixed $port
486
     */
487 328
    private function formatPort($port = null): ?int
488
    {
489 328
        if (null === $port || '' === $port) {
490 278
            return null;
491
        }
492
493 92
        if (!is_int($port) && !(is_string($port) && 1 === preg_match('/^\d*$/', $port))) {
494 2
            throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
495
        }
496
497 92
        $port = (int) $port;
498 92
        if (0 > $port) {
499 2
            throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
500
        }
501
502 92
        $defaultPort = self::SCHEME_DEFAULT_PORT[$this->scheme] ?? null;
503 92
        if ($defaultPort === $port) {
504 14
            return null;
505
        }
506
507 84
        return $port;
508
    }
509
510
    /**
511
     * {@inheritdoc}
512
     */
513 18
    public static function __set_state(array $components): self
514
    {
515 18
        $components['user'] = null;
516 18
        $components['pass'] = null;
517 18
        if (null !== $components['user_info']) {
518 14
            [$components['user'], $components['pass']] = explode(':', $components['user_info'], 2) + [1 => null];
519
        }
520
521 18
        return new self(
522 18
            $components['scheme'],
523 18
            $components['user'],
524 18
            $components['pass'],
525 18
            $components['host'],
526 18
            $components['port'],
527 18
            $components['path'],
528 18
            $components['query'],
529 18
            $components['fragment']
530
        );
531
    }
532
533
    /**
534
     * Create a new instance from a URI and a Base URI.
535
     *
536
     * The returned URI must be absolute.
537
     *
538
     * @param mixed $uri      the input URI to create
539
     * @param mixed $base_uri the base URI used for reference
540
     */
541 86
    public static function createFromBaseUri($uri, $base_uri = null): UriInterface
542
    {
543 86
        if (!$uri instanceof UriInterface) {
544 86
            $uri = self::createFromString($uri);
545
        }
546
547 86
        if (null === $base_uri) {
548 6
            if (null === $uri->getScheme()) {
549 2
                throw new SyntaxError(sprintf('the URI `%s` must be absolute', (string) $uri));
550
            }
551
552 4
            if (null === $uri->getAuthority()) {
553 2
                return $uri;
554
            }
555
556
            /** @var UriInterface $uri */
557 2
            $uri = UriResolver::resolve($uri, $uri->withFragment(null)->withQuery(null)->withPath(''));
558
559 2
            return $uri;
560
        }
561
562 80
        if (!$base_uri instanceof UriInterface) {
563 80
            $base_uri = self::createFromString($base_uri);
564
        }
565
566 80
        if (null === $base_uri->getScheme()) {
567 2
            throw new SyntaxError(sprintf('the base URI `%s` must be absolute', (string) $base_uri));
568
        }
569
570
        /** @var UriInterface $uri */
571 78
        $uri = UriResolver::resolve($uri, $base_uri);
572
573 78
        return $uri;
574
    }
575
576
    /**
577
     * Create a new instance from a string.
578
     *
579
     * @param string|mixed $uri
580
     */
581 278
    public static function createFromString($uri = ''): self
582
    {
583 278
        $components = UriString::parse($uri);
584
585 278
        return new self(
586 278
            $components['scheme'],
587 278
            $components['user'],
588 278
            $components['pass'],
589 278
            $components['host'],
590 278
            $components['port'],
591 278
            $components['path'],
592 278
            $components['query'],
593 278
            $components['fragment']
594
        );
595
    }
596
597
    /**
598
     * Create a new instance from a hash of parse_url parts.
599
     *
600
     * Create an new instance from a hash representation of the URI similar
601
     * to PHP parse_url function result
602
     */
603 90
    public static function createFromComponents(array $components = []): self
604
    {
605
        $components += [
606 90
            'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
607
            'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
608
        ];
609
610 90
        return new self(
611 90
            $components['scheme'],
612 90
            $components['user'],
613 90
            $components['pass'],
614 90
            $components['host'],
615 90
            $components['port'],
616 90
            $components['path'],
617 90
            $components['query'],
618 90
            $components['fragment']
619
        );
620
    }
621
622
    /**
623
     * Create a new instance from a data file path.
624
     *
625
     * @param resource|null $context
626
     *
627
     * @throws SyntaxError If the file does not exist or is not readable
628
     */
629 6
    public static function createFromDataPath(string $path, $context = null): self
630
    {
631 6
        $file_args = [$path, false];
632 6
        $mime_args = [$path, FILEINFO_MIME];
633 6
        if (null !== $context) {
634 4
            $file_args[] = $context;
635 4
            $mime_args[] = $context;
636
        }
637
638 6
        $raw = @file_get_contents(...$file_args);
0 ignored issues
show
Bug introduced by
$file_args is expanded, but the parameter $filename of file_get_contents() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

638
        $raw = @file_get_contents(/** @scrutinizer ignore-type */ ...$file_args);
Loading history...
639 6
        if (false === $raw) {
640 2
            throw new SyntaxError(sprintf('The file `%s` does not exist or is not readable', $path));
641
        }
642
643 4
        $mimetype = (string) (new finfo(FILEINFO_MIME))->file(...$mime_args);
0 ignored issues
show
Bug introduced by
$mime_args is expanded, but the parameter $file_name of finfo::file() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

643
        $mimetype = (string) (new finfo(FILEINFO_MIME))->file(/** @scrutinizer ignore-type */ ...$mime_args);
Loading history...
644
645 4
        return Uri::createFromComponents([
646 4
            'scheme' => 'data',
647 4
            'path' => str_replace(' ', '', $mimetype.';base64,'.base64_encode($raw)),
648
        ]);
649
    }
650
651
    /**
652
     * Create a new instance from a Unix path string.
653
     */
654 10
    public static function createFromUnixPath(string $uri = ''): self
655
    {
656 10
        $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
657 10
        if ('/' !== ($uri[0] ?? '')) {
658 4
            return Uri::createFromComponents(['path' => $uri]);
659
        }
660
661 6
        return Uri::createFromComponents(['path' => $uri, 'scheme' => 'file', 'host' => '']);
662
    }
663
664
    /**
665
     * Create a new instance from a local Windows path string.
666
     */
667 16
    public static function createFromWindowsPath(string $uri = ''): self
668
    {
669 16
        $root = '';
670 16
        if (1 === preg_match(self::REGEXP_WINDOW_PATH, $uri, $matches)) {
671 8
            $root = substr($matches['root'], 0, -1).':';
672 8
            $uri = substr($uri, strlen($root));
673
        }
674 16
        $uri = str_replace('\\', '/', $uri);
675 16
        $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
676
677
        //Local Windows absolute path
678 16
        if ('' !== $root) {
679 8
            return Uri::createFromComponents(['path' => '/'.$root.$uri, 'scheme' => 'file', 'host' => '']);
680
        }
681
682
        //UNC Windows Path
683 8
        if ('//' !== substr($uri, 0, 2)) {
684 6
            return Uri::createFromComponents(['path' => $uri]);
685
        }
686
687 2
        $parts = explode('/', substr($uri, 2), 2) + [1 => null];
688
689 2
        return Uri::createFromComponents(['host' => $parts[0], 'path' => '/'.$parts[1], 'scheme' => 'file']);
690
    }
691
692
    /**
693
     * Create a new instance from a PSR7 UriInterface object.
694
     */
695 2
    public static function createFromPsr7(Psr7UriInterface $uri): self
696
    {
697 2
        $scheme = $uri->getScheme();
698 2
        if ('' === $scheme) {
699 2
            $scheme = null;
700
        }
701
702 2
        $fragment = $uri->getFragment();
703 2
        if ('' === $fragment) {
704 2
            $fragment = null;
705
        }
706
707 2
        $query = $uri->getQuery();
708 2
        if ('' === $query) {
709 2
            $query = null;
710
        }
711
712 2
        $host = $uri->getHost();
713 2
        if ('' === $host) {
714 2
            $host = null;
715
        }
716
717 2
        $user_info = $uri->getUserInfo();
718 2
        $user = null;
719 2
        $pass = null;
720 2
        if ('' !== $user_info) {
721 2
            [$user, $pass] = explode(':', $user_info, 2) + [1 => null];
722
        }
723
724 2
        return new self(
725 2
            $scheme,
726 1
            $user,
727 1
            $pass,
728 1
            $host,
729 2
            $uri->getPort(),
730 2
            $uri->getPath(),
731 1
            $query,
732 1
            $fragment
733
        );
734
    }
735
736
    /**
737
     * Create a new instance from the environment.
738
     */
739 26
    public static function createFromServer(array $server): self
740
    {
741 26
        [$user, $pass] = self::fetchUserInfo($server);
742 26
        [$host, $port] = self::fetchHostname($server);
743 26
        [$path, $query] = self::fetchRequestUri($server);
744
745 26
        return Uri::createFromComponents([
746 26
            'scheme' => self::fetchScheme($server),
747 26
            'user' => $user,
748 26
            'pass' => $pass,
749 26
            'host' => $host,
750 26
            'port' => $port,
751 26
            'path' => $path,
752 26
            'query' => $query,
753
        ]);
754
    }
755
756
    /**
757
     * Returns the environment scheme.
758
     */
759 26
    private static function fetchScheme(array $server): string
760
    {
761 26
        $server += ['HTTPS' => ''];
762 26
        $res = filter_var($server['HTTPS'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
763
764 26
        return $res !== false ? 'https' : 'http';
765
    }
766
767
    /**
768
     * Returns the environment user info.
769
     */
770 28
    private static function fetchUserInfo(array $server): array
771
    {
772 28
        $server += ['PHP_AUTH_USER' => null, 'PHP_AUTH_PW' => null, 'HTTP_AUTHORIZATION' => ''];
773 28
        $user = $server['PHP_AUTH_USER'];
774 28
        $pass = $server['PHP_AUTH_PW'];
775 28
        if (0 === strpos(strtolower($server['HTTP_AUTHORIZATION']), 'basic')) {
776 4
            $userinfo = base64_decode(substr($server['HTTP_AUTHORIZATION'], 6), true);
777 4
            if (false === $userinfo) {
778 2
                throw new SyntaxError('The user info could not be detected');
779
            }
780 2
            [$user, $pass] = explode(':', $userinfo, 2) + [1 => null];
781
        }
782
783 26
        if (null !== $user) {
784 4
            $user = rawurlencode($user);
785
        }
786
787 26
        if (null !== $pass) {
788 4
            $pass = rawurlencode($pass);
789
        }
790
791 26
        return [$user, $pass];
792
    }
793
794
    /**
795
     * Returns the environment host.
796
     *
797
     * @throws SyntaxError If the host can not be detected
798
     */
799 28
    private static function fetchHostname(array $server): array
800
    {
801 28
        $server += ['SERVER_PORT' => null];
802 28
        if (null !== $server['SERVER_PORT']) {
803 26
            $server['SERVER_PORT'] = (int) $server['SERVER_PORT'];
804
        }
805
806 28
        if (isset($server['HTTP_HOST'])) {
807 18
            preg_match(',^(?<host>(\[.*\]|[^:])*)(\:(?<port>[^/?\#]*))?$,x', $server['HTTP_HOST'], $matches);
808
809
            return [
810 18
                $matches['host'],
811 18
                isset($matches['port']) ? (int) $matches['port'] : $server['SERVER_PORT'],
812
            ];
813
        }
814
815 10
        if (!isset($server['SERVER_ADDR'])) {
816 2
            throw new SyntaxError('The host could not be detected');
817
        }
818
819 8
        if (false === filter_var($server['SERVER_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
820 2
            $server['SERVER_ADDR'] = '['.$server['SERVER_ADDR'].']';
821
        }
822
823 8
        return [$server['SERVER_ADDR'], $server['SERVER_PORT']];
824
    }
825
826
    /**
827
     * Returns the environment path.
828
     */
829 26
    private static function fetchRequestUri(array $server): array
830
    {
831 26
        $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null];
832 26
        if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) {
833 2
            return explode('?', $server['UNENCODED_URL'], 2) + [1 => null];
834
        }
835
836 24
        if (isset($server['REQUEST_URI'])) {
837 20
            [$path, ] = explode('?', $server['REQUEST_URI'], 2);
838 20
            $query = ('' !== $server['QUERY_STRING']) ? $server['QUERY_STRING'] : null;
839
840 20
            return [$path, $query];
841
        }
842
843 4
        return [$server['PHP_SELF'], $server['QUERY_STRING']];
844
    }
845
846
    /**
847
     * Generate the URI authority part.
848
     *
849
     */
850 300
    private function setAuthority(): ?string
851
    {
852 300
        $authority = null;
853 300
        if (null !== $this->user_info) {
854 48
            $authority = $this->user_info.'@';
855
        }
856
857 300
        if (null !== $this->host) {
858 252
            $authority .= $this->host;
859
        }
860
861 300
        if (null !== $this->port) {
862 56
            $authority .= ':'.$this->port;
863
        }
864
865 300
        return $authority;
866
    }
867
868
    /**
869
     * Format the Path component.
870
     */
871 312
    private function formatPath(string $path): string
872
    {
873 312
        $path = $this->formatDataPath($path);
874
875 312
        static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/}{]++\|%(?![A-Fa-f0-9]{2}))/';
876
877 312
        $path = (string) preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $path);
878
879 312
        return $this->formatFilePath($path);
880
    }
881
882
    /**
883
     * Filter the Path component.
884
     *
885
     * @see https://tools.ietf.org/html/rfc2397
886
     *
887
     * @throws SyntaxError If the path is not compliant with RFC2397
888
     */
889 326
    private function formatDataPath(string $path): string
890
    {
891 326
        if ('data' !== $this->scheme) {
892 298
            return $path;
893
        }
894
895 28
        if ('' == $path) {
896 2
            return 'text/plain;charset=us-ascii,';
897
        }
898
899 26
        if (false === mb_detect_encoding($path, 'US-ASCII', true) || false === strpos($path, ',')) {
900 4
            throw new SyntaxError(sprintf('The path `%s` is invalid according to RFC2937', $path));
901
        }
902
903 22
        $parts = explode(',', $path, 2) + [1 => null];
904 22
        $mediatype = explode(';', (string) $parts[0], 2) + [1 => null];
905 22
        $data = (string) $parts[1];
906 22
        $mimetype = $mediatype[0];
907 22
        if (null === $mimetype || '' === $mimetype) {
908 4
            $mimetype = 'text/plain';
909
        }
910
911 22
        $parameters = $mediatype[1];
912 22
        if (null === $parameters || '' === $parameters) {
913 6
            $parameters = 'charset=us-ascii';
914
        }
915
916 22
        $this->assertValidPath($mimetype, $parameters, $data);
917
918 14
        return $mimetype.';'.$parameters.','.$data;
919
    }
920
921
    /**
922
     * Assert the path is a compliant with RFC2397.
923
     *
924
     * @see https://tools.ietf.org/html/rfc2397
925
     *
926
     * @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397
927
     */
928 22
    private function assertValidPath(string $mimetype, string $parameters, string $data): void
929
    {
930 22
        if (1 !== preg_match(self::REGEXP_MIMETYPE, $mimetype)) {
931 2
            throw new SyntaxError(sprintf('The path mimetype `%s` is invalid', $mimetype));
932
        }
933
934 20
        $is_binary = 1 === preg_match(self::REGEXP_BINARY, $parameters, $matches);
935 20
        if ($is_binary) {
936 8
            $parameters = substr($parameters, 0, - strlen($matches[0]));
937
        }
938
939 20
        $res = array_filter(array_filter(explode(';', $parameters), [$this, 'validateParameter']));
940 20
        if ([] !== $res) {
941 4
            throw new SyntaxError(sprintf('The path paremeters `%s` is invalid', $parameters));
942
        }
943
944 16
        if (!$is_binary) {
945 12
            return;
946
        }
947
948 4
        $res = base64_decode($data, true);
949 4
        if (false === $res || $data !== base64_encode($res)) {
950 2
            throw new SyntaxError(sprintf('The path data `%s` is invalid', $data));
951
        }
952 2
    }
953
954
    /**
955
     * Validate mediatype parameter.
956
     */
957 4
    private function validateParameter(string $parameter): bool
958
    {
959 4
        $properties = explode('=', $parameter);
960
961 4
        return 2 != count($properties) || strtolower($properties[0]) === 'base64';
962
    }
963
964 320
    private function formatFilePath(string $path): string
965
    {
966 320
        if ('file' !== $this->scheme) {
967 312
            return $path;
968
        }
969
970
        $replace = static function (array $matches): string {
971 2
            return $matches['delim'].str_replace('|', ':', $matches['root']).$matches['rest'];
972 8
        };
973
974 8
        return (string) preg_replace_callback(self::REGEXP_FILE_PATH, $replace, $path);
975
    }
976
977
    /**
978
     * Format the Query or the Fragment component.
979
     *
980
     * Returns a array containing:
981
     * <ul>
982
     * <li> the formatted component (a string or null)</li>
983
     * <li> a boolean flag telling wether the delimiter is to be added to the component
984
     * when building the URI string representation</li>
985
     * </ul>
986
     *
987
     * @param ?string $component
988
     */
989 306
    private function formatQueryAndFragment(?string $component): ?string
990
    {
991 306
        if (null === $component || '' === $component) {
992 282
            return $component;
993
        }
994
995 206
        static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/';
996 206
        return preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $component);
997
    }
998
999
    /**
1000
     * assert the URI internal state is valid.
1001
     *
1002
     * @see https://tools.ietf.org/html/rfc3986#section-3
1003
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
1004
     *
1005
     * @throws SyntaxError if the URI is in an invalid state according to RFC3986
1006
     * @throws SyntaxError if the URI is in an invalid state according to scheme specific rules
1007
     */
1008 354
    private function assertValidState(): void
1009
    {
1010 354
        if (null !== $this->authority && ('' !== $this->path && '/' !== $this->path[0])) {
1011 4
            throw new SyntaxError('If an authority is present the path must be empty or start with a `/`');
1012
        }
1013
1014 354
        if (null === $this->authority && 0 === strpos($this->path, '//')) {
1015 10
            throw new SyntaxError(sprintf('If there is no authority the path `%s` can not start with a `//`', $this->path));
1016
        }
1017
1018 354
        $pos = strpos($this->path, ':');
1019 354
        if (null === $this->authority
1020 354
            && null === $this->scheme
1021 354
            && false !== $pos
1022 354
            && false === strpos(substr($this->path, 0, $pos), '/')
1023
        ) {
1024 6
            throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.');
1025
        }
1026
1027 354
        $validationMethod = self::SCHEME_VALIDATION_METHOD[$this->scheme] ?? null;
1028 354
        if (null === $validationMethod || true === $this->$validationMethod()) {
1029 326
            $this->uri = null;
1030
1031 326
            return;
1032
        }
1033
1034 38
        throw new SyntaxError(sprintf('The uri `%s` is invalid for the data scheme', (string) $this));
1035
    }
1036
1037
    /**
1038
     * URI validation for URI schemes which allows only scheme and path components.
1039
     */
1040 2
    private function isUriWithSchemeAndPathOnly()
1041
    {
1042 2
        return null === $this->authority
1043 2
            && null === $this->query
1044 2
            && null === $this->fragment;
1045
    }
1046
1047
    /**
1048
     * URI validation for URI schemes which allows only scheme, host and path components.
1049
     */
1050 20
    private function isUriWithSchemeHostAndPathOnly()
1051
    {
1052 20
        return null === $this->user_info
1053 20
            && null === $this->port
1054 20
            && null === $this->query
1055 20
            && null === $this->fragment
1056 20
            && !('' != $this->scheme && null === $this->host);
1057
    }
1058
1059
    /**
1060
     * URI validation for URI schemes which disallow the empty '' host.
1061
     */
1062 246
    private function isNonEmptyHostUri()
1063
    {
1064 246
        return '' !== $this->host
1065 246
            && !(null !== $this->scheme && null === $this->host);
1066
    }
1067
1068
    /**
1069
     * URI validation for URIs schemes which disallow the empty '' host
1070
     * and forbids the fragment component.
1071
     */
1072 18
    private function isNonEmptyHostUriWithoutFragment()
1073
    {
1074 18
        return $this->isNonEmptyHostUri() && null === $this->fragment;
1075
    }
1076
1077
    /**
1078
     * URI validation for URIs schemes which disallow the empty '' host
1079
     * and forbids fragment and query components.
1080
     */
1081 22
    private function isNonEmptyHostUriWithoutFragmentAndQuery()
1082
    {
1083 22
        return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query;
1084
    }
1085
1086
    /**
1087
     * Generate the URI string representation from its components.
1088
     *
1089
     * @see https://tools.ietf.org/html/rfc3986#section-5.3
1090
     * @param ?string $scheme
1091
     * @param ?string $authority
1092
     * @param ?string $query
1093
     * @param ?string $fragment
1094
     */
1095 250
    private function getUriString(
1096
        ?string $scheme,
1097
        ?string $authority,
1098
        string $path,
1099
        ?string $query,
1100
        ?string $fragment
1101
    ): string {
1102 250
        if (null !== $scheme) {
1103 132
            $scheme = $scheme.':';
1104
        }
1105
1106 250
        if (null !== $authority) {
1107 122
            $authority = '//'.$authority;
1108
        }
1109
1110 250
        if (null !== $query) {
1111 40
            $query = '?'.$query;
1112
        }
1113
1114 250
        if (null !== $fragment) {
1115 34
            $fragment = '#'.$fragment;
1116
        }
1117
1118 250
        return $scheme.$authority.$path.$query.$fragment;
1119
    }
1120
1121
    /**
1122
     * {@inheritDoc}.
1123
     */
1124 260
    public function __toString(): string
1125
    {
1126 260
        $this->uri = $this->uri ?? $this->getUriString(
1127 260
            $this->scheme,
1128 260
            $this->authority,
1129 260
            $this->path,
1130 260
            $this->query,
1131 260
            $this->fragment
1132
        );
1133
1134 260
        return $this->uri;
1135
    }
1136
1137
    /**
1138
     * {@inheritdoc}
1139
     */
1140 2
    public function jsonSerialize(): string
1141
    {
1142 2
        return $this->__toString();
1143
    }
1144
1145
    /**
1146
     * {@inheritdoc}
1147
     */
1148 2
    public function __debugInfo(): array
1149
    {
1150
        return [
1151 2
            'scheme' => $this->scheme,
1152 2
            'user_info' => isset($this->user_info) ? preg_replace(',\:(.*).?$,', ':***', $this->user_info) : null,
1153 2
            'host' => $this->host,
1154 2
            'port' => $this->port,
1155 2
            'path' => $this->path,
1156 2
            'query' => $this->query,
1157 2
            'fragment' => $this->fragment,
1158
        ];
1159
    }
1160
1161
    /**
1162
     * {@inheritDoc}.
1163
     */
1164 230
    public function getScheme(): ?string
1165
    {
1166 230
        return $this->scheme;
1167
    }
1168
1169
    /**
1170
     * {@inheritDoc}.
1171
     */
1172 198
    public function getAuthority(): ?string
1173
    {
1174 198
        return $this->authority;
1175
    }
1176
1177
    /**
1178
     * {@inheritDoc}.
1179
     */
1180 96
    public function getUserInfo(): ?string
1181
    {
1182 96
        return $this->user_info;
1183
    }
1184
1185
    /**
1186
     * {@inheritDoc}.
1187
     */
1188 2
    public function getUser(): ?string
1189
    {
1190 2
        if (null === $this->user_info) {
1191 2
            return null;
1192
        }
1193
1194 2
        [$user, ] = explode(':', $this->user_info, 2);
1195
1196 2
        return $user;
1197
    }
1198
1199
    /**
1200
     * {@inheritDoc}.
1201
     */
1202 2
    public function getPass(): ?string
1203
    {
1204 2
        if (null === $this->user_info) {
1205 2
            return null;
1206
        }
1207
1208 2
        [$user, $pass] = explode(':', $this->user_info, 2) + [1 => null];
1209
1210 2
        return $pass;
1211
    }
1212
1213
    /**
1214
     * {@inheritDoc}.
1215
     */
1216 208
    public function getHost(): ?string
1217
    {
1218 208
        return $this->host;
1219
    }
1220
1221
    /**
1222
     * {@inheritDoc}.
1223
     */
1224 238
    public function getPort(): ?int
1225
    {
1226 238
        return $this->port;
1227
    }
1228
1229
    /**
1230
     * {@inheritDoc}.
1231
     */
1232 204
    public function getPath(): string
1233
    {
1234 204
        return $this->path;
1235
    }
1236
1237
    /**
1238
     * {@inheritDoc}.
1239
     */
1240 114
    public function getQuery(): ?string
1241
    {
1242 114
        return $this->query;
1243
    }
1244
1245
    /**
1246
     * {@inheritDoc}.
1247
     */
1248 26
    public function getFragment(): ?string
1249
    {
1250 26
        return $this->fragment;
1251
    }
1252
1253
    /**
1254
     * {@inheritDoc}.
1255
     */
1256 146
    public function withScheme($scheme): UriInterface
1257
    {
1258 146
        $scheme = $this->formatScheme($this->filterString($scheme));
1259 144
        if ($scheme === $this->scheme) {
1260 10
            return $this;
1261
        }
1262
1263 136
        $clone = clone $this;
1264 136
        $clone->scheme = $scheme;
1265 136
        $clone->port = $clone->formatPort($clone->port);
1266 136
        $clone->authority = $clone->setAuthority();
1267 136
        $clone->assertValidState();
1268
1269 136
        return $clone;
1270
    }
1271
1272
    /**
1273
     * Filter a string.
1274
     *
1275
     * @param mixed $str the value to evaluate as a string
1276
     *
1277
     * @throws SyntaxError if the submitted data can not be converted to string
1278
     */
1279 206
    private function filterString($str): ?string
1280
    {
1281 206
        if (null === $str) {
1282 146
            return $str;
1283
        }
1284
1285 204
        if (!is_scalar($str) && !method_exists($str, '__toString')) {
1286 2
            throw new TypeError(sprintf('The component must be a string, a scalar or a stringable object %s given', gettype($str)));
1287
        }
1288
1289 202
        $str = (string) $str;
1290 202
        if (1 !== preg_match(self::REGEXP_INVALID_CHARS, $str)) {
1291 200
            return $str;
1292
        }
1293
1294 2
        throw new SyntaxError(sprintf('The component `%s` contains invalid characters', $str));
1295
    }
1296
1297
    /**
1298
     * {@inheritDoc}.
1299
     * @param null|mixed $password
1300
     */
1301 146
    public function withUserInfo($user, $password = null): UriInterface
1302
    {
1303 146
        $user_info = null;
1304 146
        $user = $this->filterString($user);
1305 146
        if (null !== $password) {
1306 16
            $password = $this->filterString($password);
1307
        }
1308
1309 146
        if ('' !== $user) {
1310 76
            $user_info = $this->formatUserInfo($user, $password);
1311
        }
1312
1313 146
        if ($user_info === $this->user_info) {
1314 128
            return $this;
1315
        }
1316
1317 20
        $clone = clone $this;
1318 20
        $clone->user_info = $user_info;
1319 20
        $clone->authority = $clone->setAuthority();
1320 20
        $clone->assertValidState();
1321
1322 20
        return $clone;
1323
    }
1324
1325
    /**
1326
     * {@inheritDoc}.
1327
     */
1328 178
    public function withHost($host): UriInterface
1329
    {
1330 178
        $host = $this->formatHost($this->filterString($host));
1331 176
        if ($host === $this->host) {
1332 96
            return $this;
1333
        }
1334
1335 132
        $clone = clone $this;
1336 132
        $clone->host = $host;
1337 132
        $clone->authority = $clone->setAuthority();
1338 132
        $clone->assertValidState();
1339
1340 132
        return $clone;
1341
    }
1342
1343
    /**
1344
     * {@inheritDoc}.
1345
     */
1346 136
    public function withPort($port): UriInterface
1347
    {
1348 136
        $port = $this->formatPort($port);
1349 132
        if ($port === $this->port) {
1350 130
            return $this;
1351
        }
1352
1353 4
        $clone = clone $this;
1354 4
        $clone->port = $port;
1355 4
        $clone->authority = $clone->setAuthority();
1356 4
        $clone->assertValidState();
1357
1358 4
        return $clone;
1359
    }
1360
1361
    /**
1362
     * {@inheritDoc}.
1363
     */
1364 172
    public function withPath($path): UriInterface
1365
    {
1366 172
        $path = $this->filterString($path);
1367 172
        if (null === $path) {
0 ignored issues
show
introduced by
The condition null === $path is always false.
Loading history...
1368 2
            throw new TypeError('A path must be a string NULL given');
1369
        }
1370
1371 170
        $path = $this->formatPath($path);
1372 170
        if ($path === $this->path) {
1373 34
            return $this;
1374
        }
1375
1376 158
        $clone = clone $this;
1377 158
        $clone->path = $path;
1378 158
        $clone->assertValidState();
1379
1380 146
        return $clone;
1381
    }
1382
1383
    /**
1384
     * {@inheritDoc}.
1385
     */
1386 104
    public function withQuery($query): UriInterface
1387
    {
1388 104
        $query = $this->formatQueryAndFragment($this->filterString($query));
1389 104
        if ($query === $this->query) {
1390 96
            return $this;
1391
        }
1392
1393 14
        $clone = clone $this;
1394 14
        $clone->query = $query;
1395 14
        $clone->assertValidState();
1396
1397 14
        return $clone;
1398
    }
1399
1400
    /**
1401
     * {@inheritDoc}.
1402
     */
1403 24
    public function withFragment($fragment): UriInterface
1404
    {
1405 24
        $fragment = $this->formatQueryAndFragment($this->filterString($fragment));
1406 24
        if ($fragment === $this->fragment) {
1407 24
            return $this;
1408
        }
1409
1410 4
        $clone = clone $this;
1411 4
        $clone->fragment = $fragment;
1412 4
        $clone->assertValidState();
1413
1414 4
        return $clone;
1415
    }
1416
}
1417