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;