scheme = $parsed['scheme']; } if (isset($parsed['host'])) { $this->host = strtolower($parsed['host']); } if (isset($parsed['port'])) { $this->port = $parsed['port']; } if (isset($parsed['user'])) { $this->user = $parsed['user']; } if (isset($parsed['pass'])) { $this->password = $parsed['pass']; } if (isset($parsed['path'])) { $this->path = $parsed['path']; } if (isset($parsed['query'])) { $this->query = $parsed['query']; } if (isset($parsed['fragment'])) { $this->fragment = $parsed['fragment']; } } /** * Retrieve the scheme component of the URI. * * If no scheme is present, this method will return an empty string. * * The value returned will be normalized to lowercase, per RFC 3986 * Section 3.1. * * The trailing ":" character is not part of the scheme and will not be * added. * * @see https://tools.ietf.org/html/rfc3986#section-3.1 * @return string The URI scheme. */ public function getScheme() { return $this->scheme; } /** * Retrieve the authority component of the URI. * * If no authority information is present, this method will return an empty * string. * * The authority syntax of the URI is: * *
     * [user-info@]host[:port]
     * 
* * If the port component is not set or is the standard port for the current * scheme, it will not be included. * * @see https://tools.ietf.org/html/rfc3986#section-3.2 * @return string The URI authority, in "[user-info@]host[:port]" format. */ public function getAuthority() { $host = $this->getHost(); if (!$host) { return ''; } $authority = ''; // User Info $userInfo = $this->getUserInfo(); if ($userInfo) { $authority .= $userInfo . '@'; } // Host $authority .= $host; // Port: Include only if non-standard if ($this->nonStandardPort()) { $authority .= ':' . $this->getPort(); } return $authority; } private function nonStandardPort(): bool { $port = $this->getPort(); $scheme = $this->getScheme(); return $scheme === 'http' && $port !== 80 || $scheme === 'https' && $port !== 443; } /** * Retrieve the user information component of the URI. * * If no user information is present, this method will return an empty * string. * * If a user is present in the URI, this will return that value; * additionally, if the password is also present, it will be appended to the * user value, with a colon (":") separating the values. * * The trailing "@" character is not part of the user information and will * not be added. * * @return string The URI user information, in "username[:password]" format. */ public function getUserInfo() { $userInfo = $this->user; if ($userInfo && $this->password) { $userInfo .= ':' . $this->password; } return $userInfo; } /** * Retrieve the host component of the URI. * * If no host is present, this method will return an empty string. * * The value returned will be normalized to lowercase, per RFC 3986 * Section 3.2.2. * * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 * @return string The URI host. */ public function getHost() { return $this->host; } /** * Retrieve the port component of the URI. * * If a port is present, and it is non-standard for the current scheme, * this method MUST return it as an integer. If the port is the standard port * used with the current scheme, this method SHOULD return null. * * If no port is present, and no scheme is present, this method MUST return * a null value. * * If no port is present, but a scheme is present, this method MAY return * the standard port for that scheme, but SHOULD return null. * * @return null|int The URI port. */ public function getPort() { if ($this->port === null) { switch ($this->scheme) { case 'http': return 80; case 'https': return 443; default: return null; } } return $this->port; } /** * Retrieve the path component of the URI. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * Normally, the empty path "" and absolute path "/" are considered equal as * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically * do this normalization because in contexts with a trimmed base path, e.g. * the front controller, this difference becomes significant. It's the task * of the user to handle both "" and "/". * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.3. * * As an example, if the value should include a slash ("/") not intended as * delimiter between path segments, that value MUST be passed in encoded * form (e.g., "%2F") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.3 * @return string The URI path. */ public function getPath() { if ($this->path === '*') { return $this->path; } return $this->percentEncode($this->path); } /** * Retrieve the query string of the URI. * * If no query string is present, this method MUST return an empty string. * * The leading "?" character is not part of the query and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.4. * * As an example, if a value in a key/value pair of the query string should * include an ampersand ("&") not intended as a delimiter between values, * that value MUST be passed in encoded form (e.g., "%26") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.4 * @return string The URI query string. */ public function getQuery() { return $this->percentEncode($this->query); } /** * Retrieve the fragment component of the URI. * * If no fragment is present, this method MUST return an empty string. * * The leading "#" character is not part of the fragment and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.5. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.5 * @return string The URI fragment. */ public function getFragment() { return $this->percentEncode($this->fragment); } /** * Return an instance with the specified scheme. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified scheme. * * Implementations MUST support the schemes "http" and "https" case * insensitively, and MAY accommodate other schemes if required. * * An empty scheme is equivalent to removing the scheme. * * @param string $scheme The scheme to use with the new instance. * @return static A new instance with the specified scheme. * @throws InvalidArgumentException for invalid or unsupported schemes. */ public function withScheme($scheme) { $scheme = $scheme ? strtolower($scheme) : ''; if (!in_array($scheme, ['', 'http', 'https'])) { throw new InvalidArgumentException('Scheme must be http, https, or empty.'); } $uri = clone $this; $uri->scheme = $scheme; return $uri; } /** * Return an instance with the specified user information. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified user information. * * Password is optional, but the user information MUST include the * user; an empty string for the user is equivalent to removing user * information. * * @param string $user The user name to use for authority. * @param null|string $password The password associated with $user. * @return static A new instance with the specified user information. */ public function withUserInfo($user, $password = null) { $uri = clone $this; $uri->user = $user; $uri->password = $password; return $uri; } /** * Return an instance with the specified host. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified host. * * An empty host value is equivalent to removing the host. * * @param string $host The hostname to use with the new instance. * @return static A new instance with the specified host. * @throws InvalidArgumentException for invalid hostnames. */ public function withHost($host) { if (!is_string($host)) { throw new InvalidArgumentException('Host must be a string.'); } $uri = clone $this; $uri->host = strtolower($host); return $uri; } /** * Return an instance with the specified port. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified port. * * Implementations MUST raise an exception for ports outside the * established TCP and UDP port ranges. * * A null value provided for the port is equivalent to removing the port * information. * * @param null|int $port The port to use with the new instance; a null value * removes the port information. * @return static A new instance with the specified port. * @throws InvalidArgumentException for invalid ports. */ public function withPort($port) { if (is_numeric($port)) { if ($port < self::MIN_PORT || $port > self::MAX_PORT) { $message = sprintf('Port must be between %s and %s.', self::MIN_PORT, self::MAX_PORT); throw new InvalidArgumentException($message); } $port = (int) $port; } elseif ($port !== null) { throw new InvalidArgumentException('Port must be an int or null.'); } $uri = clone $this; $uri->port = $port; return $uri; } /** * Return an instance with the specified path. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified path. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * Users can provide both encoded and decoded path characters. * Implementations ensure the correct encoding as outlined in getPath(). * * @param string $path The path to use with the new instance. * @return static A new instance with the specified path. * @throws InvalidArgumentException for invalid paths. */ public function withPath($path) { if (!is_string($path)) { throw new InvalidArgumentException('Path must be a string'); } $uri = clone $this; $uri->path = $path; return $uri; } /** * Return an instance with the specified query string. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified query string. * * Users can provide both encoded and decoded query characters. * Implementations ensure the correct encoding as outlined in getQuery(). * * An empty query string value is equivalent to removing the query string. * * @param string $query The query string to use with the new instance. * @return static A new instance with the specified query string. * @throws InvalidArgumentException for invalid query strings. */ public function withQuery($query) { $uri = clone $this; $uri->query = $query; return $uri; } /** * Return an instance with the specified URI fragment. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified URI fragment. * * Users can provide both encoded and decoded fragment characters. * Implementations ensure the correct encoding as outlined in getFragment(). * * An empty fragment value is equivalent to removing the fragment. * * @param string $fragment The fragment to use with the new instance. * @return static A new instance with the specified fragment. */ public function withFragment($fragment) { $uri = clone $this; $uri->fragment = $fragment; return $uri; } /** * Return the string representation as a URI reference. * * Depending on which components of the URI are present, the resulting * string is either a full URI or relative reference according to RFC 3985, * Section 4.1. The method concatenates the various components of the URI, * using the appropriate delimiters: * * - If a scheme is present, it MUST be suffixed by ":". * - If an authority is present, it MUST be prefixed by "//". * - The path can be concatenated without delimiters. But there are two * cases where the path has to be adjusted to make the URI reference * valid as PHP does not allow to throw an exception in __toString(): * - If the path is rootless and an authority is present, the path MUST * be prefixed by "/". * - If the path is starting with more than one "/" and no authority is * present, the starting slashes MUST be reduced to one. * - If a query is present, it MUST be prefixed by "?". * - If a fragment is present, it MUST be prefixed by "#". * * @see http://tools.ietf.org/html/rfc3986#section-4.1 * @return string */ public function __toString() { $string = ''; $authority = $this->getAuthority(); if ($authority !== '') { $scheme = $this->getScheme(); if ($scheme !== '') { $string = $scheme . ':'; } $string .= "//$authority"; } $path = $this->getPath(); if ($path !== '') { $string .= $path; } $query = $this->getQuery(); if ($query !== '') { $string .= "?$query"; } $fragment = $this->getFragment(); if ($fragment !== '') { $string .= "#$fragment"; } return $string; } /** * Return a percent-encoded string. * * This method encode each character that is not: * - A percent sign ("%") that is followed by a hex character (0-9, a-f, A-F) * - An "unreserved character" per RFC 3986 (see below) * - A "reserved character" per RFC 3986 (see below) * * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * reserved = gen-delims / sub-delims * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" * * @param string $subject * @return string */ private function percentEncode($subject) { $reserved = ':/?#[]@!$&\'()*+,;='; $reserved = preg_quote($reserved); $pattern = '~(?:%(?![a-fA-F0-9]{2}))|(?:[^%a-zA-Z0-9\-\.\_\~' . $reserved . ']{1})~'; $callback = function (array $matches): string { return urlencode($matches[0]); }; return preg_replace_callback($pattern, $callback, $subject); } }