Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.06% covered (warning)
64.06%
41 / 64
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemporaryStore
64.06% covered (warning)
64.06%
41 / 64
40.00% covered (danger)
40.00%
4 / 10
57.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 hasId
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
4.12
 getOrCreateId
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 apply
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 getFilePath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 import
31.58% covered (danger)
31.58%
6 / 19
0.00% covered (danger)
0.00%
0 / 1
17.53
 push
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 peek
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 pop
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 remove
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Store;
6
7use DateInterval;
8use PeServer\Core\Collection\Arr;
9use PeServer\Core\Cryptography;
10use PeServer\Core\IO\Directory;
11use PeServer\Core\IO\File;
12use PeServer\Core\IO\IOUtility;
13use PeServer\Core\IO\Path;
14use PeServer\Core\Store\CookieStore;
15use PeServer\Core\Store\TemporaryOptions;
16use PeServer\Core\Text;
17use PeServer\Core\Throws\ArgumentException;
18use PeServer\Core\Throws\ArgumentNullException;
19use PeServer\Core\Throws\InvalidOperationException;
20use PeServer\Core\Utc;
21
22/**
23 * 一時データ管理処理。
24 *
25 * CookieStore を使ってセッションではない何かを扱う。
26 *
27 * アプリケーション側で明示的に使用しない想定。
28 */
29class 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}