Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
66 / 66
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
UrlQuery
100.00% covered (success)
100.00%
66 / 66
100.00% covered (success)
100.00%
6 / 6
33
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
13
 from
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
10
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 toString
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Web;
6
7use Stringable;
8use PeServer\Core\Binary;
9use PeServer\Core\Collection\Arr;
10use PeServer\Core\Collection\Collections;
11use PeServer\Core\Encoding;
12use PeServer\Core\Text;
13use PeServer\Core\Throws\ArgumentException;
14use PeServer\Core\Throws\ArgumentNullException;
15use PeServer\Core\Throws\InvalidOperationException;
16use PeServer\Core\TypeUtility;
17
18/**
19 * URL のクエリ構成要素。
20 *
21 * 並びなぁ、結構つらめ
22 */
23readonly 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}