public function withHost($host)
$host = $this->filterHost($host);
if ($this->host === $host) {
public function withPort($port)
$port = $this->filterPort($port);
if ($this->port === $port) {
$new->removeDefaultPort();
public function withPath($path)
$path = $this->filterPath($path);
if ($this->path === $path) {
public function withQuery($query)
$query = $this->filterQueryAndFragment($query);
if ($this->query === $query) {
public function withFragment($fragment)
$fragment = $this->filterQueryAndFragment($fragment);
if ($this->fragment === $fragment) {
$new->fragment = $fragment;
* Apply parse_url parts to a URI.
* @param array $parts Array of parse_url parts to apply.
private function applyParts(array $parts)
$this->scheme = isset($parts['scheme'])
? $this->filterScheme($parts['scheme'])
$this->userInfo = isset($parts['user'])
? $this->filterUserInfoComponent($parts['user'])
$this->host = isset($parts['host'])
? $this->filterHost($parts['host'])
$this->port = isset($parts['port'])
? $this->filterPort($parts['port'])
$this->path = isset($parts['path'])
? $this->filterPath($parts['path'])
$this->query = isset($parts['query'])
? $this->filterQueryAndFragment($parts['query'])
$this->fragment = isset($parts['fragment'])
? $this->filterQueryAndFragment($parts['fragment'])
if (isset($parts['pass'])) {
$this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']);
$this->removeDefaultPort();
* @throws \InvalidArgumentException If the scheme is invalid.
private function filterScheme($scheme)
if (!is_string($scheme)) {
throw new \InvalidArgumentException('Scheme must be a string');
return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
* @param string $component
* @throws \InvalidArgumentException If the user info is invalid.
private function filterUserInfoComponent($component)
if (!is_string($component)) {
throw new \InvalidArgumentException('User info must be a string');
return preg_replace_callback(
'/(?:[^%' . self::$charUnreserved . self::$charSubDelims . ']+|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
* @throws \InvalidArgumentException If the host is invalid.
private function filterHost($host)
throw new \InvalidArgumentException('Host must be a string');
return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
* @throws \InvalidArgumentException If the port is invalid.
private function filterPort($port)
if (0 > $port || 0xffff < $port) {
throw new \InvalidArgumentException(
sprintf('Invalid port: %d. Must be between 0 and 65535', $port)
* @param UriInterface $uri
private static function getFilteredQueryString(UriInterface $uri, array $keys)
$current = $uri->getQuery();
$decodedKeys = array_map('rawurldecode', $keys);
return array_filter(explode('&', $current), function ($part) use ($decodedKeys) {
return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true);
* @param string|null $value
private static function generateQueryString($key, $value)
// Query string separators ("=", "&") within the key or value need to be encoded
// (while preventing double-encoding) before setting the query string. All other
// chars that need percent-encoding will be encoded by withQuery().
$queryString = strtr($key, self::$replaceQuery);
$queryString .= '=' . strtr($value, self::$replaceQuery);
private function removeDefaultPort()
if ($this->port !== null && self::isDefaultPort($this)) {
* Filters the path of a URI
* @throws \InvalidArgumentException If the path is invalid.
private function filterPath($path)
throw new \InvalidArgumentException('Path must be a string');
return preg_replace_callback(
'/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
* Filters the query string or fragment of a URI.
* @throws \InvalidArgumentException If the query or fragment is invalid.
private function filterQueryAndFragment($str)
throw new \InvalidArgumentException('Query and fragment must be a string');
return preg_replace_callback(
'/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
private function rawurlencodeMatchZero(array $match)
return rawurlencode($match[0]);
private function validateState()
if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) {
$this->host = self::HTTP_DEFAULT_HOST;
if ($this->getAuthority() === '') {
if (0 === strpos($this->path, '//')) {
throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"');
if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) {
throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon');
} elseif (isset($this->path[0]) && $this->path[0] !== '/') {
'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' .
'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.',
$this->path = '/' . $this->path;
//throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty');