Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
66 / 66 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
1 / 1 |
UrlQuery | |
100.00% |
66 / 66 |
|
100.00% |
6 / 6 |
33 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
13 | |||
from | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
10 | |||
isEmpty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getQuery | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
toString | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
__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\ArgumentNullException; |
15 | use PeServer\Core\Throws\InvalidOperationException; |
16 | use PeServer\Core\TypeUtility; |
17 | |
18 | /** |
19 | * URL のクエリ構成要素。 |
20 | * |
21 | * 並びなぁ、結構つらめ |
22 | */ |
23 | readonly class UrlQuery implements Stringable |
24 | { |
25 | #region variable |
26 | |
27 | /** |
28 | * 構成要素 |
29 | * |
30 | * `null` の場合はほんとになんもない(`?` がない) |
31 | * 配列要素数が 0 の場合は `?` のみ |
32 | * |
33 | * @var array<string,(string|null)[]>|null |
34 | */ |
35 | private array|null $items; |
36 | |
37 | |
38 | #endregion |
39 | |
40 | /** |
41 | * 生成 |
42 | * |
43 | * @param string|array<string,(string|null)[]>|null $query クエリ。配列を渡す場合は `from` を使用すること(ある程度補完される)。 |
44 | * @param UrlEncoding|null $urlEncoding |
45 | */ |
46 | public function __construct(string|array|null $query, ?UrlEncoding $urlEncoding = null) |
47 | { |
48 | if ($query === null) { |
49 | $this->items = null; |
50 | } elseif (is_array($query)) { |
51 | foreach ($query as $key => $value) { |
52 | if (!is_array($value)) { //@phpstan-ignore-line [DOCTYPE] |
53 | throw new ArgumentException("\$query: [$key] value not array"); |
54 | } |
55 | foreach ($value as $i => $v) { |
56 | if (!($v === null || is_string($v))) { //@phpstan-ignore-line [DOCTYPE] |
57 | $s = TypeUtility::getType($v); |
58 | throw new ArgumentException("\$query: [$key] value $i:[$s] not string|null"); |
59 | } |
60 | } |
61 | } |
62 | $this->items = $query; |
63 | } else { |
64 | $urlEncoding ??= UrlEncoding::createDefault(); |
65 | |
66 | /** @var array<string,(string|null)[]> */ |
67 | $work = []; |
68 | |
69 | $queries = Text::split($query, '&'); |
70 | foreach ($queries as $rawQuery) { |
71 | $rawKeyValues = Text::split($rawQuery, '=', 2); |
72 | $rawKey = $rawKeyValues[0]; |
73 | if (Text::isNullOrEmpty($rawKey)) { |
74 | continue; |
75 | } |
76 | $rawValue = isset($rawKeyValues[1]) ? $rawKeyValues[1] : null; |
77 | |
78 | $key = $urlEncoding->decode($rawKey); |
79 | if (!isset($work[$key])) { |
80 | $work[$key] = []; |
81 | } |
82 | if ($rawValue === null) { |
83 | $work[$key][] = null; |
84 | } else { |
85 | $value = $urlEncoding->decode($rawValue); |
86 | $work[$key][] = $value; |
87 | } |
88 | } |
89 | |
90 | $this->items = $work; |
91 | } |
92 | } |
93 | |
94 | #region function |
95 | |
96 | /** |
97 | * 配列から生成。 |
98 | * |
99 | * 少しくらいは融通をつける。 |
100 | * NOTE: bool はどう変換(t/true/TRUE/on)すればいいか分からんので面倒見ない |
101 | * |
102 | * @param array<string,(string|int|null)[]|string|int|null> $query |
103 | * @return self |
104 | */ |
105 | public static function from(array $query, ?UrlEncoding $urlEncoding = null): self |
106 | { |
107 | /** @var array<string,(string|null)[]> */ |
108 | $workQuery = []; |
109 | |
110 | foreach ($query as $key => $value) { |
111 | /** @var (string|null)[] */ |
112 | $workValues = []; |
113 | |
114 | if ($value !== null) { |
115 | if (is_string($value)) { |
116 | $workValues = [$value]; |
117 | } elseif (is_int($value)) { |
118 | $workValues = [(string)$value]; |
119 | } elseif (is_array($value)) { |
120 | foreach ($value as $i => $v) { |
121 | if ($v === null || is_string($v)) { |
122 | $workValues[] = $v; |
123 | } elseif (is_int($v)) { //@phpstan-ignore-line [DOCTYPE] |
124 | $workValues[] = (string)$v; |
125 | } else { |
126 | $s = TypeUtility::getType($v); |
127 | throw new ArgumentException("\$query: [$key] value $i:[$s] not string|int|null"); |
128 | } |
129 | } |
130 | } else { |
131 | throw new ArgumentException("\$query: [$key] value not string|int|array|null"); |
132 | } |
133 | } |
134 | |
135 | $workQuery[$key] = $workValues; |
136 | } |
137 | |
138 | return new self($workQuery, $urlEncoding); |
139 | } |
140 | |
141 | /** |
142 | * クエリ部分が完全に存在しないか。 |
143 | * |
144 | * @return bool |
145 | * @phpstan-assert-if-true null $this->items |
146 | * @phpstan-assert-if-false array<string,(string|null)[]> $this->items |
147 | */ |
148 | public function isEmpty(): bool |
149 | { |
150 | return $this->items === null; |
151 | } |
152 | |
153 | /** |
154 | * 現在のクエリ状態を取得。 |
155 | * |
156 | * @return array<string,(string|null)[]> |
157 | */ |
158 | public function getQuery(): array |
159 | { |
160 | if ($this->isEmpty()) { |
161 | throw new InvalidOperationException('empty'); |
162 | } |
163 | |
164 | return $this->items; |
165 | } |
166 | |
167 | |
168 | /** |
169 | * Undocumented function |
170 | * |
171 | * @param UrlEncoding $urlEncoding |
172 | * @return string |
173 | */ |
174 | public function toString(?UrlEncoding $urlEncoding = null): string |
175 | { |
176 | if ($this->isEmpty()) { |
177 | return Text::EMPTY; |
178 | } |
179 | |
180 | if (!Arr::getCount($this->items)) { |
181 | return '?'; |
182 | } |
183 | |
184 | $urlEncoding ??= UrlEncoding::createDefault(); |
185 | |
186 | $kvItems = []; |
187 | foreach ($this->items as $key => $values) { |
188 | $encKey = $urlEncoding->encode($key); |
189 | foreach ($values as $value) { |
190 | if ($value === null) { |
191 | $kvItems[] = $encKey; |
192 | } else { |
193 | $encValue = $urlEncoding->encode($value); |
194 | $kvItems[] = "$encKey=$encValue"; |
195 | } |
196 | } |
197 | } |
198 | |
199 | return '?' . Text::join('&', $kvItems); |
200 | } |
201 | |
202 | #endregion |
203 | |
204 | #region Stringable |
205 | |
206 | public function __toString(): string |
207 | { |
208 | return $this->toString(); |
209 | } |
210 | |
211 | #endregion |
212 | } |