Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
123 / 123 |
|
100.00% |
14 / 14 |
CRAP | |
100.00% |
1 / 1 |
Url | |
100.00% |
123 / 123 |
|
100.00% |
14 / 14 |
28 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDecodedValue | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
tryParse | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
6 | |||
parse | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
changeScheme | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
changeAuthentication | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
clearAuthentication | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
changeHost | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
changePort | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
changePath | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
changeQuery | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
changeFragment | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
toString | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
8 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace PeServer\Core\Web; |
6 | |
7 | use Stringable; |
8 | use PeServer\Core\Binary; |
9 | use PeServer\Core\Collection\Arr; |
10 | use PeServer\Core\Collection\Collections; |
11 | use PeServer\Core\Encoding; |
12 | use PeServer\Core\Text; |
13 | use PeServer\Core\Throws\ArgumentException; |
14 | use PeServer\Core\Throws\ParseException; |
15 | use PeServer\Core\Web\UrlEncoding; |
16 | use PeServer\Core\Web\UrlPath; |
17 | use PeServer\Core\Web\UrlQuery; |
18 | |
19 | /** |
20 | * URL。 |
21 | */ |
22 | |
23 | readonly class Url implements Stringable |
24 | { |
25 | /** |
26 | * 生成。 |
27 | * |
28 | * 基本的には `tryParse`/`parse` を使用する想定。 |
29 | * |
30 | * @param non-empty-string $scheme |
31 | * @param string $user |
32 | * @param string $password |
33 | * @param non-empty-string $host |
34 | * @param int|null $port `null` は未指定 |
35 | * @phpstan-param int<0,65535>|null $port |
36 | * @param UrlPath $path |
37 | * @param UrlQuery $query `null` は未指定 |
38 | * @param string|null $fragment `null` は未指定 |
39 | */ |
40 | public function __construct( |
41 | public string $scheme, |
42 | public string $user, |
43 | public string $password, |
44 | public string $host, |
45 | public ?int $port, |
46 | public UrlPath $path, |
47 | public UrlQuery $query, |
48 | public ?string $fragment |
49 | ) { |
50 | } |
51 | |
52 | #region |
53 | |
54 | /** |
55 | * 配列からURL・文字列をデコードした値を取得。 |
56 | * |
57 | * @param array<array-key,mixed> $elements |
58 | * @param string $key |
59 | * @param string|null $default |
60 | * @param UrlEncoding $urlEncoding |
61 | * @return string|null デコードされた値か、取得失敗時のデフォルト値 |
62 | */ |
63 | private static function getDecodedValue(array $elements, string $key, ?string $default, UrlEncoding $urlEncoding): ?string |
64 | { |
65 | $rawElement = $elements[$key] ?? $default; |
66 | if (Text::isNullOrWhiteSpace($rawElement)) { |
67 | return $rawElement; |
68 | } |
69 | |
70 | return $urlEncoding->decode($rawElement); |
71 | } |
72 | |
73 | /** |
74 | * 文字列から生成。 |
75 | * |
76 | * `parse_url` ラッパー。 |
77 | * |
78 | * @param string $url URL 文字列。 |
79 | * @param self|null $result 結果格納。成功時に格納される。 |
80 | * @param UrlEncoding|null $urlEncoding URLエンコーディング。 |
81 | * @return bool 成功。 |
82 | * @phpstan-assert-if-true self $result |
83 | * @phpstan-assert-if-false null $result |
84 | */ |
85 | public static function tryParse(string $url, ?self &$result, ?UrlEncoding $urlEncoding = null): bool |
86 | { |
87 | $urlEncoding ??= UrlEncoding::createDefault(); |
88 | |
89 | $elements = parse_url($url); |
90 | if ($elements === false) { |
91 | return false; |
92 | } |
93 | |
94 | // さすがにこの二点は保証しておきたい(scheme は微妙と思いつつ) |
95 | if (!isset($elements['scheme']) || Text::isNullOrWhiteSpace($elements['scheme'])) { |
96 | return false; |
97 | } |
98 | if (!array_key_exists('host', $elements) || Text::isNullOrWhiteSpace($elements['host'])) { |
99 | return false; |
100 | } |
101 | |
102 | /** @var string */ |
103 | $user = self::getDecodedValue($elements, 'user', Text::EMPTY, $urlEncoding); |
104 | /** @var string */ |
105 | $pass = self::getDecodedValue($elements, 'pass', Text::EMPTY, $urlEncoding); |
106 | $fragment = self::getDecodedValue($elements, 'fragment', null, $urlEncoding); |
107 | |
108 | $result = new self( |
109 | $elements['scheme'], |
110 | $user, |
111 | $pass, |
112 | $elements['host'], |
113 | $elements['port'] ?? null, |
114 | new UrlPath($elements['path'] ?? Text::EMPTY), |
115 | new UrlQuery($elements['query'] ?? null, $urlEncoding), |
116 | $fragment |
117 | ); |
118 | |
119 | return true; |
120 | } |
121 | |
122 | /** |
123 | * 文字列から生成。 |
124 | * |
125 | * 失敗時に例外。 |
126 | * |
127 | * @param string $url URL 文字列。 |
128 | * @param UrlEncoding|null $urlEncoding URLエンコーディング。 |
129 | * @return self |
130 | */ |
131 | public static function parse(string $url, ?UrlEncoding $urlEncoding = null): self |
132 | { |
133 | if (self::tryParse($url, $result, $urlEncoding)) { |
134 | return $result; |
135 | } |
136 | throw new ParseException(); |
137 | } |
138 | |
139 | /** |
140 | * スキームを変更。 |
141 | * |
142 | * @param non-empty-string $scheme |
143 | * @return self 変更された新規URL |
144 | */ |
145 | public function changeScheme(string $scheme): self |
146 | { |
147 | return new self( |
148 | $scheme, |
149 | $this->user, |
150 | $this->password, |
151 | $this->host, |
152 | $this->port, |
153 | $this->path, |
154 | $this->query, |
155 | $this->fragment |
156 | ); |
157 | } |
158 | |
159 | /** |
160 | * 認証情報を変更。 |
161 | * |
162 | * @param string $user |
163 | * @param string $password |
164 | * @return self 変更された新規URL |
165 | */ |
166 | public function changeAuthentication(string $user, string $password): self |
167 | { |
168 | return new self( |
169 | $this->scheme, |
170 | $user, |
171 | $password, |
172 | $this->host, |
173 | $this->port, |
174 | $this->path, |
175 | $this->query, |
176 | $this->fragment |
177 | ); |
178 | } |
179 | |
180 | /** |
181 | * 認証情報を破棄。 |
182 | * |
183 | * @return self 変更された新規URL |
184 | */ |
185 | public function clearAuthentication(): self |
186 | { |
187 | return $this->changeAuthentication(Text::EMPTY, Text::EMPTY); |
188 | } |
189 | |
190 | |
191 | /** |
192 | * ホストを変更。 |
193 | * |
194 | * @param non-empty-string $host |
195 | * @return self 変更された新規URL |
196 | */ |
197 | public function changeHost(string $host): self |
198 | { |
199 | return new self( |
200 | $this->scheme, |
201 | $this->user, |
202 | $this->password, |
203 | $host, |
204 | $this->port, |
205 | $this->path, |
206 | $this->query, |
207 | $this->fragment |
208 | ); |
209 | } |
210 | |
211 | /** |
212 | * ポートを変更。 |
213 | * |
214 | * @param int $port |
215 | * @phpstan-param int<0,65535>|null $port |
216 | * @return self 変更された新規URL |
217 | */ |
218 | public function changePort(?int $port): self |
219 | { |
220 | return new self( |
221 | $this->scheme, |
222 | $this->user, |
223 | $this->password, |
224 | $this->host, |
225 | $port, |
226 | $this->path, |
227 | $this->query, |
228 | $this->fragment |
229 | ); |
230 | } |
231 | |
232 | /** |
233 | * パスを変更。 |
234 | * |
235 | * @param UrlPath $path |
236 | * @return self 変更された新規URL |
237 | */ |
238 | public function changePath(UrlPath $path): self |
239 | { |
240 | return new self( |
241 | $this->scheme, |
242 | $this->user, |
243 | $this->password, |
244 | $this->host, |
245 | $this->port, |
246 | $path, |
247 | $this->query, |
248 | $this->fragment |
249 | ); |
250 | } |
251 | |
252 | /** |
253 | * クエリを変更。 |
254 | * |
255 | * @param UrlQuery $query |
256 | * @return self 変更された新規URL |
257 | */ |
258 | public function changeQuery(UrlQuery $query): self |
259 | { |
260 | return new self( |
261 | $this->scheme, |
262 | $this->user, |
263 | $this->password, |
264 | $this->host, |
265 | $this->port, |
266 | $this->path, |
267 | $query, |
268 | $this->fragment |
269 | ); |
270 | } |
271 | |
272 | /** |
273 | * フラグメントを変更。 |
274 | * |
275 | * @param string|null $fragment |
276 | * @return self 変更された新規URL |
277 | */ |
278 | public function changeFragment(?string $fragment): self |
279 | { |
280 | return new self( |
281 | $this->scheme, |
282 | $this->user, |
283 | $this->password, |
284 | $this->host, |
285 | $this->port, |
286 | $this->path, |
287 | $this->query, |
288 | $fragment |
289 | ); |
290 | } |
291 | |
292 | public function toString(?UrlEncoding $urlEncoding = null, bool $trailingSlash = false): string |
293 | { |
294 | $urlEncoding ??= UrlEncoding::createDefault(); |
295 | |
296 | $work = $this->scheme . '://'; |
297 | |
298 | $hasAuth = false; |
299 | if (!Text::isNullOrEmpty($this->user)) { |
300 | $work .= $urlEncoding->encode($this->user); |
301 | $hasAuth = true; |
302 | } |
303 | if (!Text::isNullOrEmpty($this->password)) { |
304 | $work .= ':' . $urlEncoding->encode($this->password); |
305 | $hasAuth = true; |
306 | } |
307 | if ($hasAuth) { |
308 | $work .= '@'; |
309 | } |
310 | |
311 | $work .= $this->host; |
312 | if ($this->port !== null) { |
313 | $work .= ':' . (string)$this->port; |
314 | } |
315 | if ($this->path->isEmpty() && $trailingSlash) { |
316 | $work .= '/'; |
317 | } else { |
318 | $work .= $this->path->toString($trailingSlash); |
319 | } |
320 | $work .= $this->query->toString($urlEncoding); |
321 | |
322 | if ($this->fragment !== null) { |
323 | $work .= '#' . $urlEncoding->encode($this->fragment); |
324 | } |
325 | |
326 | return $work; |
327 | } |
328 | |
329 | #endregion |
330 | |
331 | #region Stringable |
332 | |
333 | public function __toString(): string |
334 | { |
335 | return $this->toString(); |
336 | } |
337 | |
338 | #endregion |
339 | } |