Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.25% |
263 / 265 |
|
95.65% |
22 / 23 |
CRAP | |
0.00% |
0 / 1 |
FileCache | |
99.25% |
263 / 265 |
|
95.65% |
22 / 23 |
126 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
connect | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
3.14 | |||
flushAll | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
stats | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getThreshold | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
set | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
add | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
build | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
stringify | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
9 | |||
getExpire | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
get | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
11 | |||
reverseValue | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
13 | |||
delete | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
11 | |||
exists | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
11 | |||
increment | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 | |||
decrement | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 | |||
rename | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getLike | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
11 | |||
deleteLike | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
11 | |||
updateExpire | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
flush | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
8 | |||
replace | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
getPath | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\DataStorage\Cache\Connection |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\DataStorage\Cache\Connection; |
16 | |
17 | use phpOMS\DataStorage\Cache\CacheStatus; |
18 | use phpOMS\DataStorage\Cache\CacheType; |
19 | use phpOMS\DataStorage\Cache\Exception\InvalidConnectionConfigException; |
20 | use phpOMS\Stdlib\Base\Exception\InvalidEnumValue; |
21 | use phpOMS\System\File\Local\Directory; |
22 | use phpOMS\System\File\Local\File; |
23 | |
24 | /** |
25 | * File cache. |
26 | * |
27 | * This implementation uses the hard drive as cache by saving data to the disc as text files. |
28 | * The text files follow a defined strucuture which allows this implementation to parse the cached data. |
29 | * |
30 | * Allowed datatypes: null, int, bool, float, string, \DateTime, \JsonSerializable, \Serializable |
31 | * File structure: |
32 | * data type (1 byte) |
33 | * delimiter (1 byte) |
34 | * expiration duration in seconds (1 - n bytes) (based on the file creation date) |
35 | * delimiter (1 byte) |
36 | * data (n bytes) |
37 | * |
38 | * @package phpOMS\DataStorage\Cache\Connection |
39 | * @license OMS License 2.0 |
40 | * @link https://jingga.app |
41 | * @since 1.0.0 |
42 | */ |
43 | final class FileCache extends ConnectionAbstract |
44 | { |
45 | /** |
46 | * {@inheritdoc} |
47 | */ |
48 | protected string $type = CacheType::FILE; |
49 | |
50 | /** |
51 | * Delimiter for cache meta data |
52 | * |
53 | * @var string |
54 | * @since 1.0.0 |
55 | */ |
56 | private const DELIM = '$'; |
57 | |
58 | /** |
59 | * File path sanitizer |
60 | * |
61 | * @var string |
62 | * @since 1.0.0 |
63 | */ |
64 | private const SANITIZE = '~'; |
65 | |
66 | /** |
67 | * Only cache if data is larger than threshold (0-100). |
68 | * |
69 | * @var int |
70 | * @since 1.0.0 |
71 | */ |
72 | private int $threshold = 50; |
73 | |
74 | /** |
75 | * Constructor |
76 | * |
77 | * @param string $path Cache path |
78 | * |
79 | * @since 1.0.0 |
80 | */ |
81 | public function __construct(string $path) |
82 | { |
83 | $this->connect([$path]); |
84 | } |
85 | |
86 | /** |
87 | * Connect to cache |
88 | * |
89 | * @param null|array{0:string} $data Cache data (path to cache directory) |
90 | * |
91 | * @return void |
92 | * |
93 | * @throws InvalidConnectionConfigException |
94 | * |
95 | * @since 1.0.0 |
96 | */ |
97 | public function connect(array $data = null) : void |
98 | { |
99 | $this->dbdata = $data; |
100 | |
101 | if (!Directory::exists($data[0])) { |
102 | Directory::create($data[0], 0766, true); |
103 | } |
104 | |
105 | if (\realpath($data[0]) === false) { |
106 | $this->status = CacheStatus::FAILURE; |
107 | throw new InvalidConnectionConfigException((string) \json_encode($this->dbdata)); |
108 | } |
109 | |
110 | $this->status = CacheStatus::OK; |
111 | $this->con = \realpath($data[0]); |
112 | } |
113 | |
114 | /** |
115 | * {@inheritdoc} |
116 | */ |
117 | public function flushAll() : bool |
118 | { |
119 | if ($this->status !== CacheStatus::OK) { |
120 | return false; |
121 | } |
122 | |
123 | \array_map('unlink', \glob($this->con . '/*')); |
124 | |
125 | return true; |
126 | } |
127 | |
128 | /** |
129 | * {@inheritdoc} |
130 | */ |
131 | public function stats() : array |
132 | { |
133 | if ($this->status !== CacheStatus::OK) { |
134 | return []; |
135 | } |
136 | |
137 | $stats = []; |
138 | $stats['status'] = $this->status; |
139 | $stats['count'] = Directory::count($this->con); |
140 | $stats['size'] = Directory::size($this->con); |
141 | |
142 | return $stats; |
143 | } |
144 | |
145 | /** |
146 | * {@inheritdoc} |
147 | */ |
148 | public function getThreshold() : int |
149 | { |
150 | return $this->threshold; |
151 | } |
152 | |
153 | /** |
154 | * {@inheritdoc} |
155 | */ |
156 | public function set(int | string $key, mixed $value, int $expire = -1) : void |
157 | { |
158 | if ($this->status !== CacheStatus::OK) { |
159 | return; |
160 | } |
161 | |
162 | $path = Directory::sanitize((string) $key, self::SANITIZE); |
163 | |
164 | $fp = \fopen($this->con . '/' . \trim($path, '/') . '.cache', 'w+'); |
165 | if (\flock($fp, \LOCK_EX)) { |
166 | \ftruncate($fp, 0); |
167 | \fwrite($fp, $this->build($value, $expire)); |
168 | \fflush($fp); |
169 | \flock($fp, \LOCK_UN); |
170 | } |
171 | \fclose($fp); |
172 | } |
173 | |
174 | /** |
175 | * {@inheritdoc} |
176 | */ |
177 | public function add(int | string $key, mixed $value, int $expire = -1) : bool |
178 | { |
179 | if ($this->status !== CacheStatus::OK) { |
180 | return false; |
181 | } |
182 | |
183 | $path = $this->getPath($key); |
184 | |
185 | if (!File::exists($path)) { |
186 | $fp = \fopen($path, 'w+'); |
187 | if (\flock($fp, \LOCK_EX)) { |
188 | \ftruncate($fp, 0); |
189 | \fwrite($fp, $this->build($value, $expire)); |
190 | \fflush($fp); |
191 | \flock($fp, \LOCK_UN); |
192 | } |
193 | \fclose($fp); |
194 | |
195 | return true; |
196 | } |
197 | |
198 | return false; |
199 | } |
200 | |
201 | /** |
202 | * Removing all cache elements larger or equal to the expiration date. Call flushAll for removing persistent cache elements (expiration is negative) as well. |
203 | * |
204 | * @param mixed $value Data to cache |
205 | * @param int $expire Expire date of the cached data |
206 | * |
207 | * @return string |
208 | * |
209 | * @since 1.0.0 |
210 | */ |
211 | private function build(mixed $value, int $expire) : string |
212 | { |
213 | $type = $this->dataType($value); |
214 | $raw = $this->stringify($value, $type); |
215 | |
216 | return $type . self::DELIM . $expire . self::DELIM . $raw; |
217 | } |
218 | |
219 | /** |
220 | * Create string representation of data for storage |
221 | * |
222 | * @param mixed $value Value of the data |
223 | * @param int $type Type of the cache data |
224 | * |
225 | * @return string |
226 | * |
227 | * @throws InvalidEnumValue This exception is thrown if an unsupported cache value type is used |
228 | * |
229 | * @since 1.0.0 |
230 | */ |
231 | private function stringify(mixed $value, int $type) : string |
232 | { |
233 | if ($type === CacheValueType::_INT || $type === CacheValueType::_STRING || $type === CacheValueType::_BOOL) { |
234 | return (string) $value; |
235 | } elseif ($type === CacheValueType::_FLOAT) { |
236 | return \rtrim(\rtrim(\number_format($value, 5, '.', ''), '0'), '.'); |
237 | } elseif ($type === CacheValueType::_ARRAY) { |
238 | return (string) \json_encode($value); |
239 | } elseif ($type === CacheValueType::_SERIALIZABLE) { |
240 | return \get_class($value) . self::DELIM . $value->serialize(); |
241 | } elseif ($type === CacheValueType::_JSONSERIALIZABLE) { |
242 | return \get_class($value) . self::DELIM . ((string) \json_encode($value->jsonSerialize())); |
243 | } elseif ($type === CacheValueType::_NULL) { |
244 | return ''; |
245 | } |
246 | |
247 | throw new InvalidEnumValue($type); // @codeCoverageIgnore |
248 | } |
249 | |
250 | /** |
251 | * Get expire offset |
252 | * |
253 | * @param string $raw Raw data |
254 | * |
255 | * @return int |
256 | * |
257 | * @since 1.0.0 |
258 | */ |
259 | private function getExpire(string $raw) : int |
260 | { |
261 | $expireStart = (int) \strpos($raw, self::DELIM); |
262 | $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); |
263 | |
264 | return (int) \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); |
265 | } |
266 | |
267 | /** |
268 | * {@inheritdoc} |
269 | */ |
270 | public function get(int | string $key, int $expire = -1) : mixed |
271 | { |
272 | if ($this->status !== CacheStatus::OK) { |
273 | return null; |
274 | } |
275 | |
276 | $path = $this->getPath($key); |
277 | if (!File::exists($path)) { |
278 | return null; |
279 | } |
280 | |
281 | $created = File::created($path)->getTimestamp(); |
282 | $now = \time(); |
283 | |
284 | if ($expire >= 0 && $created + $expire < $now) { |
285 | return null; |
286 | } |
287 | |
288 | $raw = \file_get_contents($path); |
289 | if ($raw === false) { |
290 | return null; // @codeCoverageIgnore |
291 | } |
292 | |
293 | $type = (int) $raw[0]; |
294 | $expireStart = (int) \strpos($raw, self::DELIM); |
295 | $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); |
296 | |
297 | if ($expireStart < 0 || $expireEnd < 0) { |
298 | return null; // @codeCoverageIgnore |
299 | } |
300 | |
301 | $cacheExpire = \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); |
302 | $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; |
303 | |
304 | if ($cacheExpire >= 0 && $created + $cacheExpire + \max(0, $expire) < $now) { |
305 | $this->delete($key); |
306 | |
307 | return null; |
308 | } |
309 | |
310 | return $this->reverseValue($type, $raw, $expireEnd); |
311 | } |
312 | |
313 | /** |
314 | * Parse cached value |
315 | * |
316 | * @param int $type Cached value type |
317 | * @param string $raw Cached value |
318 | * @param int $expireEnd Value end position |
319 | * |
320 | * @return mixed |
321 | * |
322 | * @since 1.0.0 |
323 | */ |
324 | private function reverseValue(int $type, string $raw, int $expireEnd) : mixed |
325 | { |
326 | switch ($type) { |
327 | case CacheValueType::_INT: |
328 | return (int) \substr($raw, $expireEnd + 1); |
329 | case CacheValueType::_FLOAT: |
330 | return (float) \substr($raw, $expireEnd + 1); |
331 | case CacheValueType::_BOOL: |
332 | return (bool) \substr($raw, $expireEnd + 1); |
333 | case CacheValueType::_STRING: |
334 | return \substr($raw, $expireEnd + 1); |
335 | case CacheValueType::_ARRAY: |
336 | $array = \substr($raw, $expireEnd + 1); |
337 | return \json_decode($array === false ? '[]' : $array, true); |
338 | case CacheValueType::_NULL: |
339 | return null; |
340 | case CacheValueType::_JSONSERIALIZABLE: |
341 | $namespaceStart = (int) \strpos($raw, self::DELIM, $expireEnd); |
342 | $namespaceEnd = (int) \strpos($raw, self::DELIM, $namespaceStart + 1); |
343 | $namespace = \substr($raw, $namespaceStart + 1, $namespaceEnd - $namespaceStart - 1); |
344 | |
345 | if ($namespace === false) { |
346 | return null; // @codeCoverageIgnore |
347 | } |
348 | |
349 | return new $namespace(); |
350 | case CacheValueType::_SERIALIZABLE: |
351 | $namespaceStart = (int) \strpos($raw, self::DELIM, $expireEnd); |
352 | $namespaceEnd = (int) \strpos($raw, self::DELIM, $namespaceStart + 1); |
353 | $namespace = \substr($raw, $namespaceStart + 1, $namespaceEnd - $namespaceStart - 1); |
354 | |
355 | if ($namespace === false) { |
356 | return null; // @codeCoverageIgnore |
357 | } |
358 | |
359 | $obj = new $namespace(); |
360 | $obj->unserialize(\substr($raw, $namespaceEnd + 1)); |
361 | |
362 | return $obj; |
363 | default: |
364 | return null; // @codeCoverageIgnore |
365 | } |
366 | } |
367 | |
368 | /** |
369 | * {@inheritdoc} |
370 | */ |
371 | public function delete(int | string $key, int $expire = -1) : bool |
372 | { |
373 | if ($this->status !== CacheStatus::OK) { |
374 | return false; |
375 | } |
376 | |
377 | $path = $this->getPath($key); |
378 | if (!File::exists($path)) { |
379 | return true; |
380 | } |
381 | |
382 | if ($expire < 0) { |
383 | File::delete($path); |
384 | |
385 | return true; |
386 | } |
387 | |
388 | if ($expire >= 0) { |
389 | $created = File::created($path)->getTimestamp(); |
390 | $now = \time(); |
391 | $raw = \file_get_contents($path); |
392 | |
393 | if ($raw === false) { |
394 | return false; // @codeCoverageIgnore |
395 | } |
396 | |
397 | $cacheExpire = $this->getExpire($raw); |
398 | $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; |
399 | |
400 | if (($cacheExpire >= 0 && $created + $cacheExpire < $now) |
401 | || ($cacheExpire >= 0 && \abs($now - $created) > $expire) |
402 | ) { |
403 | File::delete($path); |
404 | |
405 | return true; |
406 | } |
407 | } |
408 | |
409 | return false; |
410 | } |
411 | |
412 | /** |
413 | * {@inheritdoc} |
414 | */ |
415 | public function exists(int | string $key, int $expire = -1) : bool |
416 | { |
417 | if ($this->status !== CacheStatus::OK) { |
418 | return false; |
419 | } |
420 | |
421 | $path = $this->getPath($key); |
422 | if (!File::exists($path)) { |
423 | return false; |
424 | } |
425 | |
426 | $created = File::created($path)->getTimestamp(); |
427 | $now = \time(); |
428 | |
429 | if ($expire >= 0 && $created + $expire < $now) { |
430 | return false; |
431 | } |
432 | |
433 | $raw = \file_get_contents($path); |
434 | if ($raw === false) { |
435 | return false; // @codeCoverageIgnore |
436 | } |
437 | |
438 | $expireStart = (int) \strpos($raw, self::DELIM); |
439 | $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); |
440 | |
441 | if ($expireStart < 0 || $expireEnd < 0) { |
442 | return false; // @codeCoverageIgnore |
443 | } |
444 | |
445 | $cacheExpire = \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); |
446 | $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; |
447 | |
448 | if ($cacheExpire >= 0 && $created + $cacheExpire + \max(0, $expire) < $now) { |
449 | File::delete($path); |
450 | |
451 | return false; |
452 | } |
453 | |
454 | return true; |
455 | } |
456 | |
457 | /** |
458 | * {@inheritdoc} |
459 | */ |
460 | public function increment(int | string $key, int $value = 1) : bool |
461 | { |
462 | if ($this->status !== CacheStatus::OK) { |
463 | return false; |
464 | } |
465 | |
466 | $path = $this->getPath($key); |
467 | if (!File::exists($path)) { |
468 | return false; |
469 | } |
470 | |
471 | $created = File::created($path)->getTimestamp(); |
472 | |
473 | $raw = \file_get_contents($path); |
474 | if ($raw === false) { |
475 | return false; // @codeCoverageIgnore |
476 | } |
477 | |
478 | $type = (int) $raw[0]; |
479 | $expireStart = (int) \strpos($raw, self::DELIM); |
480 | $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); |
481 | |
482 | if ($expireStart < 0 || $expireEnd < 0) { |
483 | return false; // @codeCoverageIgnore |
484 | } |
485 | |
486 | $cacheExpire = \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); |
487 | $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; |
488 | |
489 | $val = $this->reverseValue($type, $raw, $expireEnd); |
490 | $this->set($key, $val + $value, $cacheExpire); |
491 | |
492 | return true; |
493 | } |
494 | |
495 | /** |
496 | * {@inheritdoc} |
497 | */ |
498 | public function decrement(int | string $key, int $value = 1) : bool |
499 | { |
500 | if ($this->status !== CacheStatus::OK) { |
501 | return false; |
502 | } |
503 | |
504 | $path = $this->getPath($key); |
505 | if (!File::exists($path)) { |
506 | return false; |
507 | } |
508 | |
509 | $created = File::created($path)->getTimestamp(); |
510 | |
511 | $raw = \file_get_contents($path); |
512 | if ($raw === false) { |
513 | return false; // @codeCoverageIgnore |
514 | } |
515 | |
516 | $type = (int) $raw[0]; |
517 | $expireStart = (int) \strpos($raw, self::DELIM); |
518 | $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); |
519 | |
520 | if ($expireStart < 0 || $expireEnd < 0) { |
521 | return false; // @codeCoverageIgnore |
522 | } |
523 | |
524 | $cacheExpire = \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); |
525 | $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; |
526 | |
527 | $val = $this->reverseValue($type, $raw, $expireEnd); |
528 | $this->set($key, $val - $value, $cacheExpire); |
529 | |
530 | return true; |
531 | } |
532 | |
533 | /** |
534 | * {@inheritdoc} |
535 | */ |
536 | public function rename(int | string $old, int | string $new, int $expire = -1) : bool |
537 | { |
538 | if ($this->status !== CacheStatus::OK) { |
539 | return false; |
540 | } |
541 | |
542 | $value = $this->get($old); |
543 | $this->set($new, $value, $expire); |
544 | $this->delete($old); |
545 | |
546 | return true; |
547 | } |
548 | |
549 | /** |
550 | * {@inheritdoc} |
551 | */ |
552 | public function getLike(string $pattern, int $expire = -1) : array |
553 | { |
554 | if ($this->status !== CacheStatus::OK) { |
555 | return []; |
556 | } |
557 | |
558 | $files = Directory::list($this->con . '/', $pattern . '\.cache', true); |
559 | $values = []; |
560 | |
561 | foreach ($files as $path) { |
562 | $path = $this->con . '/' . $path; |
563 | $created = File::created($path)->getTimestamp(); |
564 | $now = \time(); |
565 | |
566 | if ($expire >= 0 && $created + $expire < $now) { |
567 | continue; |
568 | } |
569 | |
570 | $raw = \file_get_contents($path); |
571 | if ($raw === false) { |
572 | continue; // @codeCoverageIgnore |
573 | } |
574 | |
575 | $type = (int) $raw[0]; |
576 | $expireStart = (int) \strpos($raw, self::DELIM); |
577 | $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); |
578 | |
579 | if ($expireStart < 0 || $expireEnd < 0) { |
580 | continue; // @codeCoverageIgnore |
581 | } |
582 | |
583 | $cacheExpire = \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); |
584 | $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; |
585 | |
586 | if ($cacheExpire >= 0 && $created + $cacheExpire + \max(0, $expire) < $now) { |
587 | File::delete($path); |
588 | |
589 | continue; |
590 | } |
591 | |
592 | $values[] = $this->reverseValue($type, $raw, $expireEnd); |
593 | } |
594 | |
595 | return $values; |
596 | } |
597 | |
598 | /** |
599 | * {@inheritdoc} |
600 | */ |
601 | public function deleteLike(string $pattern, int $expire = -1) : bool |
602 | { |
603 | if ($this->status !== CacheStatus::OK) { |
604 | return false; |
605 | } |
606 | |
607 | $files = Directory::list($this->con . '/', $pattern . '\.cache', true); |
608 | |
609 | foreach ($files as $path) { |
610 | $path = $this->con . '/' . $path; |
611 | |
612 | if ($expire < 0) { |
613 | File::delete($path); |
614 | |
615 | continue; |
616 | } |
617 | |
618 | if ($expire >= 0) { |
619 | $created = File::created($path)->getTimestamp(); |
620 | $now = \time(); |
621 | $raw = \file_get_contents($path); |
622 | |
623 | if ($raw === false) { |
624 | continue; // @codeCoverageIgnore |
625 | } |
626 | |
627 | $cacheExpire = $this->getExpire($raw); |
628 | $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; |
629 | |
630 | if (($cacheExpire >= 0 && $created + $cacheExpire < $now) |
631 | || ($cacheExpire >= 0 && \abs($now - $created) > $expire) |
632 | ) { |
633 | File::delete($path); |
634 | |
635 | continue; |
636 | } |
637 | } |
638 | } |
639 | |
640 | return true; |
641 | } |
642 | |
643 | /** |
644 | * {@inheritdoc} |
645 | */ |
646 | public function updateExpire(int | string $key, int $expire = -1) : bool |
647 | { |
648 | if ($this->status !== CacheStatus::OK) { |
649 | return false; |
650 | } |
651 | |
652 | $value = $this->get($key, $expire); |
653 | $this->delete($key); |
654 | $this->set($key, $value, $expire); |
655 | |
656 | return true; |
657 | } |
658 | |
659 | /** |
660 | * {@inheritdoc} |
661 | */ |
662 | public function flush(int $expire = 0) : bool |
663 | { |
664 | if ($this->status !== CacheStatus::OK) { |
665 | return false; |
666 | } |
667 | |
668 | $dir = new Directory($this->con); |
669 | $now = \time(); |
670 | |
671 | foreach ($dir as $file) { |
672 | if ($file instanceof File) { |
673 | $created = $file->createdAt->getTimestamp(); |
674 | if (($expire >= 0 && $created + $expire < $now) |
675 | || ($expire < 0 && $created + $this->getExpire($file->getContent()) < $now) |
676 | ) { |
677 | File::delete($file->getPath()); |
678 | } |
679 | } |
680 | } |
681 | |
682 | return true; |
683 | } |
684 | |
685 | /** |
686 | * {@inheritdoc} |
687 | */ |
688 | public function replace(int | string $key, mixed $value, int $expire = -1) : bool |
689 | { |
690 | if ($this->status !== CacheStatus::OK) { |
691 | return false; |
692 | } |
693 | |
694 | $path = $this->getPath($key); |
695 | |
696 | if (File::exists($path)) { |
697 | $fp = \fopen($path, 'w+'); |
698 | if (\flock($fp, \LOCK_EX)) { |
699 | \ftruncate($fp, 0); |
700 | \fwrite($fp, $this->build($value, $expire)); |
701 | \fflush($fp); |
702 | \flock($fp, \LOCK_UN); |
703 | } |
704 | \fclose($fp); |
705 | |
706 | return true; |
707 | } |
708 | |
709 | return false; |
710 | } |
711 | |
712 | /** |
713 | * Get cache path |
714 | * |
715 | * @param mixed $key Key for cached value |
716 | * |
717 | * @return string Path to cache file |
718 | * |
719 | * @since 1.0.0 |
720 | */ |
721 | private function getPath(int | string $key) : string |
722 | { |
723 | $path = Directory::sanitize((string) $key, self::SANITIZE); |
724 | return $this->con . '/' . \trim($path, '/') . '.cache'; |
725 | } |
726 | } |