Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.73% |
48 / 66 |
|
53.85% |
7 / 13 |
CRAP | |
0.00% |
0 / 1 |
SessionStore | |
72.73% |
48 / 66 |
|
53.85% |
7 / 13 |
48.26 | |
0.00% |
0 / 1 |
__construct | |
44.44% |
4 / 9 |
|
0.00% |
0 / 1 |
6.74 | |||
setApplyState | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
applyCore | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
apply | |
75.00% |
12 / 16 |
|
0.00% |
0 / 1 |
11.56 | |||
isStarted | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
start | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
3.00 | |||
restart | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
shutdown | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
isChanged | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
set | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
remove | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
getOr | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
tryGet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace PeServer\Core\Store; |
6 | |
7 | use PeServer\Core\Collection\Arr; |
8 | use PeServer\Core\IO\Directory; |
9 | use PeServer\Core\IO\IOUtility; |
10 | use PeServer\Core\Web\WebSecurity; |
11 | use PeServer\Core\Store\CookieStores; |
12 | use PeServer\Core\Store\SessionOptions; |
13 | use PeServer\Core\Text; |
14 | use PeServer\Core\Throws\ArgumentException; |
15 | use PeServer\Core\Throws\InvalidOperationException; |
16 | use PeServer\Core\Throws\NotImplementedException; |
17 | |
18 | /** |
19 | * セッション管理処理。 |
20 | * |
21 | * 一時データとして扱い、最後にセッションへ反映する感じで動く。 |
22 | * アプリケーション側で明示的に使用しない想定。 |
23 | * @SuppressWarnings(PHPMD.Superglobals) |
24 | */ |
25 | class SessionStore |
26 | { |
27 | #region define |
28 | |
29 | public const APPLY_NORMAL = 0; |
30 | public const APPLY_CANCEL = 1; |
31 | public const APPLY_RESTART = 2; |
32 | public const APPLY_SHUTDOWN = 3; |
33 | |
34 | #endregion |
35 | |
36 | #region variable |
37 | |
38 | private readonly SessionOptions $options; |
39 | private readonly CookieStore $cookie; |
40 | |
41 | /** |
42 | * セッション一時データ。 |
43 | * |
44 | * @var array<string,mixed> |
45 | */ |
46 | private array $values = []; |
47 | /** |
48 | * セッションは開始されているか。 |
49 | */ |
50 | private bool $isStarted = false; |
51 | |
52 | /** |
53 | * セッション適用状態。 |
54 | * |
55 | * @var int |
56 | * @phpstan-var self::APPLY_* |
57 | */ |
58 | private int $applyState = self::APPLY_NORMAL; |
59 | |
60 | /** |
61 | * セッションの値に変更があったか。 |
62 | */ |
63 | private bool $isChanged = false; |
64 | |
65 | #endregion |
66 | |
67 | /** |
68 | * 生成 |
69 | * |
70 | * @param SessionOptions $options セッション設定。 |
71 | * @param CookieStore $cookie Cookie 設定。 |
72 | */ |
73 | public function __construct(SessionOptions $options, CookieStore $cookie, private WebSecurity $webSecurity) |
74 | { |
75 | if (Text::isNullOrWhiteSpace($options->name)) { //@phpstan-ignore-line [DOCTYPE] |
76 | throw new ArgumentException('$options->name'); |
77 | } |
78 | |
79 | $this->options = $options; |
80 | $this->cookie = $cookie; |
81 | |
82 | if ($this->cookie->tryGet($this->options->name, $nameValue)) { |
83 | if (!Text::isNullOrWhiteSpace($nameValue)) { |
84 | $this->start(); |
85 | $this->values = $_SESSION; |
86 | $this->isStarted = true; |
87 | } |
88 | } |
89 | } |
90 | |
91 | #region function |
92 | |
93 | /** |
94 | * セッション適用状態を設定。 |
95 | * |
96 | * @param int $state |
97 | * @phpstan-param self::APPLY_* $state |
98 | * @return int |
99 | * @phpstan-return self::APPLY_* |
100 | */ |
101 | public function setApplyState(int $state): int |
102 | { |
103 | $oldValue = $this->applyState; |
104 | |
105 | $this->applyState = $state; |
106 | |
107 | return $oldValue; |
108 | } |
109 | |
110 | private function applyCore(): void |
111 | { |
112 | $csrfKey = $this->webSecurity->getCsrfKind(WebSecurity::CSRF_KIND_SESSION_KEY); |
113 | $csrfToken = $this->webSecurity->generateCsrfToken(); |
114 | $this->set($csrfKey, $csrfToken); |
115 | |
116 | $_SESSION = $this->values; |
117 | |
118 | session_write_close(); |
119 | } |
120 | |
121 | /** |
122 | * 一時セッションデータを事前指定された適用種別に応じて反映。 |
123 | * |
124 | * @return void |
125 | */ |
126 | public function apply(): void |
127 | { |
128 | switch ($this->applyState) { |
129 | case self::APPLY_NORMAL: |
130 | if ($this->isChanged()) { |
131 | if (!$this->isStarted()) { |
132 | $this->start(); |
133 | } |
134 | $this->applyCore(); |
135 | } |
136 | break; |
137 | |
138 | case self::APPLY_CANCEL: |
139 | // なんもしない |
140 | break; |
141 | |
142 | case self::APPLY_RESTART: |
143 | if ($this->isStarted()) { |
144 | $this->restart(); |
145 | } else { |
146 | $this->start(); |
147 | } |
148 | $this->applyCore(); |
149 | break; |
150 | |
151 | case self::APPLY_SHUTDOWN: |
152 | if ($this->isStarted()) { |
153 | $this->shutdown(); |
154 | } |
155 | break; |
156 | |
157 | default: |
158 | throw new NotImplementedException(); |
159 | } |
160 | } |
161 | |
162 | /** |
163 | * セッションは開始されているか。 |
164 | * |
165 | * @return boolean |
166 | */ |
167 | public function isStarted(): bool |
168 | { |
169 | return $this->isStarted; |
170 | } |
171 | |
172 | /** |
173 | * セッション開始。 |
174 | * |
175 | * @return void |
176 | * @throws InvalidOperationException 既にセッションが開始されている。 |
177 | */ |
178 | public function start(): void |
179 | { |
180 | if ($this->isStarted) { |
181 | throw new InvalidOperationException(); |
182 | } |
183 | |
184 | // セッション名はコンストラクタ時点で設定済みのためチェックしない |
185 | session_name($this->options->name); |
186 | |
187 | if (!Text::isNullOrWhiteSpace($this->options->savePath)) { |
188 | Directory::createDirectoryIfNotExists($this->options->savePath); |
189 | session_save_path($this->options->savePath); |
190 | } |
191 | |
192 | $sessionOption = [ |
193 | 'lifetime' => $this->options->cookie->getExpires(), |
194 | 'path' => $this->options->cookie->path, |
195 | 'domain' => Text::EMPTY, |
196 | 'secure' => $this->options->cookie->secure, |
197 | 'httponly' => $this->options->cookie->httpOnly, |
198 | 'samesite' => $this->options->cookie->sameSite, |
199 | ]; |
200 | session_set_cookie_params($sessionOption); |
201 | |
202 | session_start(); |
203 | } |
204 | |
205 | /** |
206 | * セッションIDの再採番。 |
207 | * |
208 | * @return void |
209 | * @throws InvalidOperationException セッションが開始されていない。 |
210 | */ |
211 | public function restart(): void |
212 | { |
213 | if (!$this->isStarted) { |
214 | throw new InvalidOperationException(); |
215 | } |
216 | } |
217 | |
218 | /** |
219 | * セッションの終了。 |
220 | * |
221 | * @return void |
222 | */ |
223 | public function shutdown(): void |
224 | { |
225 | $_SESSION = []; |
226 | |
227 | if (!$this->isStarted) { |
228 | return; |
229 | } |
230 | |
231 | $this->cookie->remove($this->options->name); |
232 | session_destroy(); |
233 | } |
234 | |
235 | |
236 | /** |
237 | * セッションは変更されているか。 |
238 | * |
239 | * @return boolean |
240 | */ |
241 | public function isChanged(): bool |
242 | { |
243 | return $this->isChanged; |
244 | } |
245 | |
246 | /** |
247 | * セッションデータ設定。 |
248 | * |
249 | * @param string $key |
250 | * @param mixed $value |
251 | * @phpstan-param ServerStoreValueAlias $value |
252 | * @return void |
253 | */ |
254 | public function set(string $key, mixed $value): void |
255 | { |
256 | $this->values[$key] = $value; |
257 | $this->isChanged = true; |
258 | } |
259 | |
260 | /** |
261 | * セッションデータ破棄。 |
262 | * |
263 | * @param string $key 対象セッションキー。空白指定ですべて削除。 |
264 | * @return void |
265 | */ |
266 | public function remove(string $key): void |
267 | { |
268 | if (Text::isNullOrEmpty($key)) { |
269 | $this->values = []; |
270 | } else { |
271 | unset($this->values[$key]); |
272 | } |
273 | |
274 | $this->isChanged = true; |
275 | } |
276 | |
277 | /** |
278 | * セッションデータ取得。 |
279 | * |
280 | * @param string $key |
281 | * @param mixed $fallbackValue |
282 | * @phpstan-param ServerStoreValueAlias $fallbackValue |
283 | * @return mixed 取得データ。 |
284 | * @phpstan-return ServerStoreValueAlias |
285 | */ |
286 | public function getOr(string $key, mixed $fallbackValue): mixed |
287 | { |
288 | return $this->values[$key] ?? $fallbackValue; |
289 | } |
290 | |
291 | /** |
292 | * セッションデータ取得。 |
293 | * |
294 | * @param string $key |
295 | * @param mixed $result |
296 | * @phpstan-param ServerStoreValueAlias $result |
297 | * @return boolean 取得できたか。 |
298 | */ |
299 | public function tryGet(string $key, mixed &$result): bool |
300 | { |
301 | return Arr::tryGet($this->values, $key, $result); |
302 | } |
303 | |
304 | #endregion |
305 | } |