Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
64.06% |
41 / 64 |
|
40.00% |
4 / 10 |
CRAP | |
0.00% |
0 / 1 |
TemporaryStore | |
64.06% |
41 / 64 |
|
40.00% |
4 / 10 |
57.38 | |
0.00% |
0 / 1 |
__construct | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
3.58 | |||
hasId | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
4.12 | |||
getOrCreateId | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
apply | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
4.00 | |||
getFilePath | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
import | |
31.58% |
6 / 19 |
|
0.00% |
0 / 1 |
17.53 | |||
push | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
5.67 | |||
peek | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
pop | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
remove | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace PeServer\Core\Store; |
6 | |
7 | use DateInterval; |
8 | use PeServer\Core\Collection\Arr; |
9 | use PeServer\Core\Cryptography; |
10 | use PeServer\Core\IO\Directory; |
11 | use PeServer\Core\IO\File; |
12 | use PeServer\Core\IO\IOUtility; |
13 | use PeServer\Core\IO\Path; |
14 | use PeServer\Core\Store\CookieStore; |
15 | use PeServer\Core\Store\TemporaryOptions; |
16 | use PeServer\Core\Text; |
17 | use PeServer\Core\Throws\ArgumentException; |
18 | use PeServer\Core\Throws\ArgumentNullException; |
19 | use PeServer\Core\Throws\InvalidOperationException; |
20 | use PeServer\Core\Utc; |
21 | |
22 | /** |
23 | * 一時データ管理処理。 |
24 | * |
25 | * CookieStore を使ってセッションではない何かを扱う。 |
26 | * |
27 | * アプリケーション側で明示的に使用しない想定。 |
28 | */ |
29 | class TemporaryStore |
30 | { |
31 | #region define |
32 | |
33 | private const ID_LENGTH = 40; |
34 | |
35 | #endregion |
36 | |
37 | #region variable |
38 | |
39 | /** |
40 | * 一時データ。 |
41 | * |
42 | * @var array<string,mixed> |
43 | * @phpstan-var array<string,ServerStoreValueAlias> |
44 | */ |
45 | private array $values = []; |
46 | |
47 | /** |
48 | * 破棄データ。 |
49 | * |
50 | * @var string[] |
51 | */ |
52 | private array $removes = []; |
53 | |
54 | /** |
55 | * 取り込み処理が行われたか。 |
56 | */ |
57 | private bool $isImported = false; |
58 | |
59 | #endregion |
60 | |
61 | public function __construct( |
62 | private readonly TemporaryOptions $options, |
63 | private readonly CookieStore $cookie |
64 | ) { |
65 | if (Text::isNullOrWhiteSpace($options->name)) { |
66 | throw new ArgumentException('$options->name'); |
67 | } |
68 | if (Text::isNullOrWhiteSpace($options->savePath)) { |
69 | throw new ArgumentException('$options->savePath'); |
70 | } |
71 | ArgumentNullException::throwIfNull($options->cookie->span, '$options->cookie->span'); |
72 | } |
73 | |
74 | #region function |
75 | |
76 | private function hasId(): bool |
77 | { |
78 | if ($this->cookie->tryGet($this->options->name, $nameValue)) { |
79 | if (!Text::isNullOrWhiteSpace($nameValue)) { |
80 | return true; |
81 | } |
82 | } |
83 | |
84 | return false; |
85 | } |
86 | |
87 | private function getOrCreateId(): string |
88 | { |
89 | if ($this->hasId()) { |
90 | return $this->cookie->getOr($this->options->name, Text::EMPTY); |
91 | } |
92 | |
93 | return Cryptography::generateRandomString(self::ID_LENGTH, Cryptography::FILE_RANDOM_STRING); |
94 | } |
95 | |
96 | public function apply(): void |
97 | { |
98 | $id = $this->getOrCreateId(); |
99 | |
100 | $path = $this->getFilePath($id); |
101 | |
102 | $this->import($id); |
103 | |
104 | foreach ($this->removes as $key) { |
105 | unset($this->values[$key]); |
106 | } |
107 | |
108 | if (Arr::getCount($this->values)) { |
109 | $this->cookie->set($this->options->name, $id, $this->options->cookie); |
110 | |
111 | Directory::createParentDirectoryIfNotExists($path); |
112 | File::writeJsonFile($path, [ |
113 | 'timestamp' => Utc::createString(), |
114 | 'values' => $this->values |
115 | ]); |
116 | } else { |
117 | $this->cookie->remove($this->options->name); |
118 | |
119 | if (File::exists($path)) { |
120 | File::removeFile($path); |
121 | } |
122 | } |
123 | } |
124 | |
125 | private function getFilePath(string $id): string |
126 | { |
127 | $path = Path::combine($this->options->savePath, "$id.json"); |
128 | return $path; |
129 | } |
130 | |
131 | private function import(string $id): void |
132 | { |
133 | if ($this->isImported) { |
134 | return; |
135 | } |
136 | |
137 | $this->isImported = true; |
138 | |
139 | $path = $this->getFilePath($id); |
140 | if (!File::exists($path)) { |
141 | return; |
142 | } |
143 | |
144 | /** @var array<string,mixed> */ |
145 | $json = File::readJsonFile($path); |
146 | |
147 | /** @var string */ |
148 | $timestamp = $json['timestamp'] ?? Text::EMPTY; |
149 | if (Text::isNullOrWhiteSpace($timestamp)) { |
150 | return; |
151 | } |
152 | if (!Utc::tryParse($timestamp, $datetime)) { |
153 | return; |
154 | } |
155 | /** @var \DateInterval */ |
156 | $span = $this->options->cookie->span; |
157 | $saveTimestamp = $datetime->add($span); |
158 | $currentTimestamp = Utc::create(); |
159 | |
160 | if ($saveTimestamp < $currentTimestamp) { |
161 | return; |
162 | } |
163 | |
164 | /** @var array<string,mixed> */ |
165 | $values = $json['values'] ?? []; |
166 | $this->values = array_replace($values, $this->values); |
167 | } |
168 | |
169 | /** |
170 | * 追加。 |
171 | * |
172 | * @param string $key |
173 | * @param mixed $value |
174 | * @phpstan-param ServerStoreValueAlias $value |
175 | * @return void |
176 | */ |
177 | public function push(string $key, mixed $value): void |
178 | { |
179 | $this->values[$key] = $value; |
180 | |
181 | if (Arr::containsValue($this->removes, $key)) { |
182 | $index = array_search($key, $this->removes); |
183 | if ($index === false) { |
184 | throw new InvalidOperationException(); |
185 | } |
186 | unset($this->removes[$index]); |
187 | } |
188 | } |
189 | |
190 | /** |
191 | * Undocumented function |
192 | * |
193 | * @param string $key |
194 | * @return mixed |
195 | * @phpstan-return ServerStoreValueAlias |
196 | */ |
197 | public function peek(string $key): mixed |
198 | { |
199 | $id = $this->getOrCreateId(); |
200 | $this->import($id); |
201 | |
202 | if (!Arr::containsKey($this->values, $key)) { |
203 | return null; |
204 | } |
205 | |
206 | return $this->values[$key]; |
207 | } |
208 | |
209 | /** |
210 | * Undocumented function |
211 | * |
212 | * @param string $key |
213 | * @return mixed |
214 | * @phpstan-return ServerStoreValueAlias |
215 | */ |
216 | public function pop(string $key): mixed |
217 | { |
218 | $value = $this->peek($key); |
219 | |
220 | $this->removes[] = $key; |
221 | |
222 | return $value; |
223 | } |
224 | |
225 | public function remove(string $key): void |
226 | { |
227 | unset($this->values[$key]); |
228 | $this->removes[] = $key; |
229 | } |
230 | |
231 | #endregion |
232 | } |