1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
<?php
declare(strict_types=1);
final class UrlException extends \Exception
{
}
/**
* Intentionally restrictive url parser.
*
* Only absolute http/https urls.
*/
final class Url
{
private string $scheme;
private string $host;
private int $port;
private string $path;
private ?string $queryString;
private function __construct()
{
}
public static function fromString(string $url): self
{
if (!self::validate($url)) {
throw new UrlException(sprintf('Illegal url: "%s"', $url));
}
$parts = parse_url($url);
if ($parts === false) {
throw new UrlException(sprintf('Failed to parse_url(): %s', $url));
}
return (new self())
->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));
}
if (str_starts_with($path, '//')) {
throw new UrlException(sprintf('Illegal path (too many forward slashes): %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
);
}
}
|