Passed
Pull Request — master (#128)
by ignace nyamagana
02:19
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 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
c 1
b 0
f 0
nc 1
nop 8
dl 0
loc 19
ccs 10
cts 10
cp 1
crap 1
rs 9.9666

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\Contracts\UriInterface;
18
use League\Uri\Exceptions\IdnSupportMissing;
19
use League\Uri\Exceptions\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 302
    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 302
        $this->scheme = $this->formatScheme($scheme);
268 302
        $this->user_info = $this->formatUserInfo($user, $pass);
269 302
        $this->host = $this->formatHost($host);
270 302
        $this->port = $this->formatPort($port);
271 302
        $this->authority = $this->setAuthority();
272 302
        $this->path = $this->formatPath($path);
273 302
        $this->query = $this->formatQueryAndFragment($query);
274 302
        $this->fragment = $this->formatQueryAndFragment($fragment);
275 302
        $this->assertValidState();
276 288
    }
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 308
    private function formatScheme(?string $scheme): ?string
286
    {
287 308
        if ('' === $scheme || null === $scheme) {
288 234
            return $scheme;
289
        }
290
291 240
        $formatted_scheme = strtolower($scheme);
292 240
        if (1 === preg_match(self::REGEXP_SCHEME, $formatted_scheme)) {
293 240
            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 334
    private function formatHost(?string $host): ?string
336
    {
337 334
        if (null === $host || '' === $host) {
338 234
            return $host;
339
        }
340
341 268
        if ('[' !== $host[0]) {
342 268
            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 266
    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 266
        $formatted_host = rawurldecode($host);
366 266
        if (1 === preg_match(self::REGEXP_HOST_REGNAME, $formatted_host)) {
367 258
            $formatted_host = strtolower($formatted_host);
368 258
            if (false === strpos($formatted_host, 'xn--')) {
369 254
                return $formatted_host;
370
            }
371
372
            // @codeCoverageIgnoreStart
373
            if (!$idn_support) {
374
                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));
375
            }
376
            // @codeCoverageIgnoreEnd
377
378 8
            $unicode = idn_to_utf8(
379 8
                $host,
380 8
                IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_UNICODE,
381 8
                INTL_IDNA_VARIANT_UTS46,
382 4
                $arr
383
            );
384
385 8
            if (0 !== $arr['errors']) {
386 2
                throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors'])));
387
            }
388
389
            // @codeCoverageIgnoreStart
390
            if (false === $unicode) {
391
                throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
392
            }
393
            // @codeCoverageIgnoreEnd
394
395 6
            return $formatted_host;
396
        }
397
398 14
        if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, $formatted_host)) {
399 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : a registered name can not contain URI delimiters or spaces', $host));
400
        }
401
402
        // @codeCoverageIgnoreStart
403
        if (!$idn_support) {
404
            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));
405
        }
406
        // @codeCoverageIgnoreEnd
407
408 12
        $formatted_host = idn_to_ascii(
409 12
            $formatted_host,
410 12
            IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_ASCII,
411 12
            INTL_IDNA_VARIANT_UTS46,
412 12
            $arr
413
        );
414 12
        if (0 !== $arr['errors']) {
415 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors'])));
416
        }
417
418
        // @codeCoverageIgnoreStart
419
        if (false === $formatted_host) {
420
            throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
421
        }
422
        // @codeCoverageIgnoreEnd
423
424 10
        return $arr['result'];
425
    }
426
427
    /**
428
     * Retrieves and format IDNA conversion error message.
429
     *
430
     * @see http://icu-project.org/apiref/icu4j/com/ibm/icu/text/IDNA.Error.html
431
     */
432 4
    private function getIDNAErrors(int $error_byte): string
433
    {
434 4
        $res = [];
435 4
        foreach (self::IDNA_ERRORS as $error => $reason) {
436 4
            if ($error === ($error_byte & $error)) {
437 4
                $res[] = $reason;
438
            }
439
        }
440
441 4
        return [] === $res ? 'Unknown IDNA conversion error.' : implode(', ', $res).'.';
442
    }
443
444
    /**
445
     * Validate and Format the IPv6/IPvfuture host.
446
     *
447
     * @throws SyntaxError if the submitted host is not a valid IP host
448
     */
449 16
    private function formatIp(string $host): string
450
    {
451 16
        $ip = substr($host, 1, -1);
452 16
        if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
453 2
            return $host;
454
        }
455
456 14
        if (1 === preg_match(self::REGEXP_HOST_IPFUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) {
457 2
            return $host;
458
        }
459
460 12
        $pos = strpos($ip, '%');
461 12
        if (false === $pos) {
462 4
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
463
        }
464
465 8
        if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, rawurldecode(substr($ip, $pos)))) {
466 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
467
        }
468
469 6
        $ip = substr($ip, 0, $pos);
470 6
        if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
471 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
472
        }
473
474
        //Only the address block fe80::/10 can have a Zone ID attach to
475
        //let's detect the link local significant 10 bits
476 4
        if (0 === strpos((string) inet_pton($ip), self::HOST_ADDRESS_BLOCK)) {
477 2
            return $host;
478
        }
479
480 2
        throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
481
    }
482
483
    /**
484
     * Format the Port component.
485
     *
486
     * @param null|mixed $port
487
     */
488 330
    private function formatPort($port = null): ?int
489
    {
490 330
        if (null === $port || '' === $port) {
491 278
            return null;
492
        }
493
494 94
        if (!is_int($port) && !(is_string($port) && 1 === preg_match('/^\d*$/', $port))) {
495 2
            throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
496
        }
497
498 94
        $port = (int) $port;
499 94
        if (0 > $port) {
500 2
            throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
501
        }
502
503 94
        $defaultPort = self::SCHEME_DEFAULT_PORT[$this->scheme] ?? null;
504 94
        if ($defaultPort === $port) {
505 14
            return null;
506
        }
507
508 86
        return $port;
509
    }
510
511
    /**
512
     * {@inheritdoc}
513
     */
514 18
    public static function __set_state(array $components): self
515
    {
516 18
        $components['user'] = null;
517 18
        $components['pass'] = null;
518 18
        if (null !== $components['user_info']) {
519 14
            [$components['user'], $components['pass']] = explode(':', $components['user_info'], 2) + [1 => null];
520
        }
521
522 18
        return new self(
523 18
            $components['scheme'],
524 18
            $components['user'],
525 18
            $components['pass'],
526 18
            $components['host'],
527 18
            $components['port'],
528 18
            $components['path'],
529 18
            $components['query'],
530 18
            $components['fragment']
531
        );
532
    }
533
534
    /**
535
     * Create a new instance from a URI and a Base URI.
536
     *
537
     * The returned URI must be absolute.
538
     *
539
     * @param mixed $uri      the input URI to create
540
     * @param mixed $base_uri the base URI used for reference
541
     */
542 86
    public static function createFromBaseUri($uri, $base_uri = null): UriInterface
543
    {
544 86
        if (!$uri instanceof UriInterface) {
545 86
            $uri = self::createFromString($uri);
546
        }
547
548 86
        if (null === $base_uri) {
549 6
            if (null === $uri->getScheme()) {
550 2
                throw new SyntaxError(sprintf('the URI `%s` must be absolute', (string) $uri));
551
            }
552
553 4
            if (null === $uri->getAuthority()) {
554 2
                return $uri;
555
            }
556
557
            /** @var UriInterface $uri */
558 2
            $uri = UriResolver::resolve($uri, $uri->withFragment(null)->withQuery(null)->withPath(''));
559
560 2
            return $uri;
561
        }
562
563 80
        if (!$base_uri instanceof UriInterface) {
564 80
            $base_uri = self::createFromString($base_uri);
565
        }
566
567 80
        if (null === $base_uri->getScheme()) {
568 2
            throw new SyntaxError(sprintf('the base URI `%s` must be absolute', (string) $base_uri));
569
        }
570
571
        /** @var UriInterface $uri */
572 78
        $uri = UriResolver::resolve($uri, $base_uri);
573
574 78
        return $uri;
575
    }
576
577
    /**
578
     * Create a new instance from a string.
579
     *
580
     * @param string|mixed $uri
581
     */
582 280
    public static function createFromString($uri = ''): self
583
    {
584 280
        $components = UriString::parse($uri);
585
586 280
        return new self(
587 280
            $components['scheme'],
588 280
            $components['user'],
589 280
            $components['pass'],
590 280
            $components['host'],
591 280
            $components['port'],
592 280
            $components['path'],
593 280
            $components['query'],
594 280
            $components['fragment']
595
        );
596
    }
597
598
    /**
599
     * Create a new instance from a hash of parse_url parts.
600
     *
601
     * Create an new instance from a hash representation of the URI similar
602
     * to PHP parse_url function result
603
     */
604 90
    public static function createFromComponents(array $components = []): self
605
    {
606
        $components += [
607 90
            'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
608
            'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
609
        ];
610
611 90
        return new self(
612 90
            $components['scheme'],
613 90
            $components['user'],
614 90
            $components['pass'],
615 90
            $components['host'],
616 90
            $components['port'],
617 90
            $components['path'],
618 90
            $components['query'],
619 90
            $components['fragment']
620
        );
621
    }
622
623
    /**
624
     * Create a new instance from a data file path.
625
     *
626
     * @param resource|null $context
627
     *
628
     * @throws SyntaxError If the file does not exist or is not readable
629
     */
630 6
    public static function createFromDataPath(string $path, $context = null): self
631
    {
632 6
        $file_args = [$path, false];
633 6
        $mime_args = [$path, FILEINFO_MIME];
634 6
        if (null !== $context) {
635 4
            $file_args[] = $context;
636 4
            $mime_args[] = $context;
637
        }
638
639 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

639
        $raw = @file_get_contents(/** @scrutinizer ignore-type */ ...$file_args);
Loading history...
640 6
        if (false === $raw) {
641 2
            throw new SyntaxError(sprintf('The file `%s` does not exist or is not readable', $path));
642
        }
643
644 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

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