23 private static $defaultPorts = [
37 private static $charUnreserved =
'a-zA-Z0-9_\-\.~';
38 private static $charSubDelims =
'!\$&\'\(\)\*\+,;=';
39 private static $replaceQuery = [
'=' =>
'%3D',
'&' =>
'%26'];
45 private $userInfo =
'';
60 private $fragment =
'';
69 $parts = parse_url($uri);
70 if ($parts ===
false) {
71 throw new \InvalidArgumentException(
"Unable to parse URI: $uri");
73 $this->applyParts($parts);
114 public static function composeComponents($scheme, $authority, $path, $query, $fragment)
120 $uri .= $scheme .
':';
123 if ($authority !=
''|| $scheme ===
'file') {
124 $uri .=
'//' . $authority;
130 $uri .=
'?' . $query;
133 if ($fragment !=
'') {
134 $uri .=
'#' . $fragment;
152 return $uri->
getPort() ===
null
174 public static function isAbsolute(UriInterface $uri)
176 return $uri->getScheme() !==
'';
191 return $uri->getScheme() ===
'' && $uri->getAuthority() !==
'';
206 return $uri->getScheme() ===
''
207 && $uri->getAuthority() ===
''
208 && isset($uri->getPath()[0])
209 && $uri->getPath()[0] ===
'/';
244 if ($base !==
null) {
247 return ($uri->
getScheme() === $base->getScheme())
249 && ($uri->
getPath() === $base->getPath())
250 && ($uri->
getQuery() === $base->getQuery());
285 $rel =
new self($rel);
304 $result = self::getFilteredQueryString($uri, [$key]);
306 return $uri->
withQuery(implode(
'&', $result));
326 $result = self::getFilteredQueryString($uri, [$key]);
328 $result[] = self::generateQueryString($key, $value);
330 return $uri->
withQuery(implode(
'&', $result));
345 $result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
347 foreach ($keyValueArray as $key => $value) {
348 $result[] = self::generateQueryString($key, $value);
351 return $uri->
withQuery(implode(
'&', $result));
367 $uri->applyParts($parts);
368 $uri->validateState();
375 return $this->scheme;
380 $authority = $this->host;
381 if ($this->userInfo !==
'') {
382 $authority = $this->userInfo .
'@' . $authority;
385 if ($this->port !==
null) {
386 $authority .=
':' . $this->port;
394 return $this->userInfo;
419 return $this->fragment;
424 $scheme = $this->filterScheme($scheme);
426 if ($this->scheme === $scheme) {
431 $new->scheme = $scheme;
432 $new->removeDefaultPort();
433 $new->validateState();
440 $info = $this->filterUserInfoComponent($user);
441 if ($password !==
null) {
442 $info .=
':' . $this->filterUserInfoComponent($password);
445 if ($this->userInfo === $info) {
450 $new->userInfo = $info;
451 $new->validateState();
458 $host = $this->filterHost($host);
460 if ($this->host === $host) {
466 $new->validateState();
473 $port = $this->filterPort($port);
475 if ($this->port === $port) {
481 $new->removeDefaultPort();
482 $new->validateState();
489 $path = $this->filterPath($path);
491 if ($this->path === $path) {
497 $new->validateState();
504 $query = $this->filterQueryAndFragment($query);
506 if ($this->query === $query) {
511 $new->query = $query;
518 $fragment = $this->filterQueryAndFragment($fragment);
520 if ($this->fragment === $fragment) {
525 $new->fragment = $fragment;
535 private function applyParts(array $parts)
537 $this->scheme = isset($parts[
'scheme'])
538 ? $this->filterScheme($parts[
'scheme'])
540 $this->userInfo = isset($parts[
'user'])
541 ? $this->filterUserInfoComponent($parts[
'user'])
543 $this->host = isset($parts[
'host'])
544 ? $this->filterHost($parts[
'host'])
546 $this->port = isset($parts[
'port'])
547 ? $this->filterPort($parts[
'port'])
549 $this->path = isset($parts[
'path'])
550 ? $this->filterPath($parts[
'path'])
552 $this->query = isset($parts[
'query'])
553 ? $this->filterQueryAndFragment($parts[
'query'])
555 $this->fragment = isset($parts[
'fragment'])
556 ? $this->filterQueryAndFragment($parts[
'fragment'])
558 if (isset($parts[
'pass'])) {
559 $this->userInfo .=
':' . $this->filterUserInfoComponent($parts[
'pass']);
562 $this->removeDefaultPort();
572 private function filterScheme($scheme)
574 if (!is_string($scheme)) {
575 throw new \InvalidArgumentException(
'Scheme must be a string');
578 return strtolower($scheme);
588 private function filterUserInfoComponent($component)
590 if (!is_string($component)) {
591 throw new \InvalidArgumentException(
'User info must be a string');
594 return preg_replace_callback(
595 '/(?:[^%' . self::$charUnreserved . self::$charSubDelims .
']+|%(?![A-Fa-f0-9]{2}))/',
596 [$this,
'rawurlencodeMatchZero'],
608 private function filterHost($host)
610 if (!is_string($host)) {
611 throw new \InvalidArgumentException(
'Host must be a string');
614 return strtolower($host);
624 private function filterPort($port)
626 if ($port ===
null) {
631 if (0 > $port || 0xffff < $port) {
632 throw new \InvalidArgumentException(
633 sprintf(
'Invalid port: %d. Must be between 0 and 65535', $port)
646 private static function getFilteredQueryString(UriInterface $uri, array $keys)
648 $current = $uri->getQuery();
650 if ($current ===
'') {
654 $decodedKeys = array_map(
'rawurldecode', $keys);
656 return array_filter(explode(
'&', $current),
function ($part) use ($decodedKeys) {
657 return !in_array(rawurldecode(explode(
'=', $part)[0]), $decodedKeys,
true);
667 private static function generateQueryString($key, $value)
672 $queryString = strtr($key, self::$replaceQuery);
674 if ($value !==
null) {
675 $queryString .=
'=' . strtr($value, self::$replaceQuery);
681 private function removeDefaultPort()
683 if ($this->port !==
null && self::isDefaultPort($this)) {
697 private function filterPath($path)
699 if (!is_string($path)) {
700 throw new \InvalidArgumentException(
'Path must be a string');
703 return preg_replace_callback(
704 '/(?:[^' . self::$charUnreserved . self::$charSubDelims .
'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
705 [$this,
'rawurlencodeMatchZero'],
719 private function filterQueryAndFragment($str)
721 if (!is_string($str)) {
722 throw new \InvalidArgumentException(
'Query and fragment must be a string');
725 return preg_replace_callback(
726 '/(?:[^' . self::$charUnreserved . self::$charSubDelims .
'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
727 [$this,
'rawurlencodeMatchZero'],
732 private function rawurlencodeMatchZero(array $match)
734 return rawurlencode($match[0]);
737 private function validateState()
739 if ($this->host ===
'' && ($this->scheme ===
'http' || $this->scheme ===
'https')) {
744 if (0 === strpos($this->path,
'//')) {
745 throw new \InvalidArgumentException(
'The path of a URI without an authority must not start with two slashes "//"');
747 if ($this->scheme ===
'' &&
false !== strpos(explode(
'/', $this->path, 2)[0],
':')) {
748 throw new \InvalidArgumentException(
'A relative URI must not have a path beginning with a segment containing a colon');
750 } elseif (isset($this->path[0]) && $this->path[0] !==
'/') {
752 'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' .
753 'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.',
756 $this->path =
'/'. $this->path;