Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.73% covered (warning)
72.73%
48 / 66
53.85% covered (warning)
53.85%
7 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SessionStore
72.73% covered (warning)
72.73%
48 / 66
53.85% covered (warning)
53.85%
7 / 13
48.26
0.00% covered (danger)
0.00%
0 / 1
 __construct
44.44% covered (danger)
44.44%
4 / 9
0.00% covered (danger)
0.00%
0 / 1
6.74
 setApplyState
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 applyCore
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 apply
75.00% covered (warning)
75.00%
12 / 16
0.00% covered (danger)
0.00%
0 / 1
11.56
 isStarted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 start
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
3.00
 restart
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 shutdown
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 isChanged
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 remove
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getOr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tryGet
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\Store;
6
7use PeServer\Core\Collection\Arr;
8use PeServer\Core\IO\Directory;
9use PeServer\Core\IO\IOUtility;
10use PeServer\Core\Web\WebSecurity;
11use PeServer\Core\Store\CookieStores;
12use PeServer\Core\Store\SessionOptions;
13use PeServer\Core\Text;
14use PeServer\Core\Throws\ArgumentException;
15use PeServer\Core\Throws\InvalidOperationException;
16use PeServer\Core\Throws\NotImplementedException;
17
18/**
19 * セッション管理処理。
20 *
21 * 一時データとして扱い、最後にセッションへ反映する感じで動く。
22 * アプリケーション側で明示的に使用しない想定。
23 * @SuppressWarnings(PHPMD.Superglobals)
24 */
25class 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}