withScheme($parts['scheme'] ?? '') ->withHost($parts['host']) ->withPort($parts['port'] ?? 80) ->withPath($parts['path'] ?? '/') ->withQueryString($parts['query'] ?? null); // todo: add fragment } public static function validate(string $url): bool { if (strlen($url) > 1500) { return false; } $pattern = '#^https?://' // scheme . '([a-z0-9-]+\.?)+' // one or more domain names . '(\.[a-z]{1,24})?' // optional global tld . '(:\d+)?' // optional port . '($|/|\?)#i'; // end of string or slash or question mark return preg_match($pattern, $url) === 1; } public function getScheme(): string { return $this->scheme; } public function getHost(): string { return $this->host; } public function getPort(): int { return $this->port; } public function getPath(): string { return $this->path; } public function getQueryString(): string { return $this->queryString; } public function withScheme(string $scheme): self { if (!in_array($scheme, ['http', 'https'])) { throw new UrlException(sprintf('Invalid scheme %s', $scheme)); } $clone = clone $this; $clone->scheme = $scheme; return $clone; } public function withHost(string $host): self { $clone = clone $this; $clone->host = $host; return $clone; } public function withPort(int $port) { $clone = clone $this; $clone->port = $port; return $clone; } public function withPath(string $path): self { if (!str_starts_with($path, '/')) { throw new UrlException(sprintf('Path must start with forward slash: %s', $path)); } $clone = clone $this; $clone->path = $path; return $clone; } public function withQueryString(?string $queryString): self { $clone = clone $this; $clone->queryString = $queryString; return $clone; } public function __toString() { if ($this->port === 80) { $port = ''; } else { $port = ':' . $this->port; } if ($this->queryString) { $queryString = '?' . $this->queryString; } else { $queryString = ''; } return sprintf( '%s://%s%s%s%s', $this->scheme, $this->host, $port, $this->path, $queryString ); } }