Skip to content

Commit 43573e3

Browse files
committed
feat(cache): add FileCache implementation for file-based caching
- Introduce FileCache class implementing PSR-16 CacheInterface - Store cache items as serialized files with expiration timestamps - Support single and multiple get, set, delete operations - Automatically create cache directory if it does not exist - Handle cache expiration and invalid data by deleting stale files - Use hashed keys to generate cache file names in a dedicated folder Signed-off-by: guanguans <[email protected]>
1 parent 0d8380b commit 43573e3

File tree

6 files changed

+487
-2
lines changed

6 files changed

+487
-2
lines changed

composer-dependency-analyser.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
'guzzlehttp/psr7',
4747
'psr/http-factory',
4848
'psr/http-message',
49-
// 'psr/simple-cache',
5049
],
5150
[ErrorType::SHADOW_DEPENDENCY]
5251
)
@@ -60,6 +59,7 @@
6059
'illuminate/collections',
6160
'illuminate/support',
6261
'symfony/var-dumper',
62+
'psr/simple-cache',
6363
],
6464
[ErrorType::DEV_DEPENDENCY_IN_PROD]
6565
);

composer-updater

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ $status = (new SingleCommandApplication)
8282
$this->highestComposerBinary = $this->getComposerBinary($composerBinary, $highestPhpBinary);
8383
$this->composerBinary = $this->getComposerBinary($composerBinary);
8484
$this->exceptPackages = array_merge([
85-
'php',
8685
'ext-*',
86+
'php',
87+
'psr/*',
8788
], $exceptPackages);
8889
$this->exceptDependencyVersions = array_merge([
8990
'\*',

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"phpstan/phpstan-deprecation-rules": "^2.0",
110110
"phpstan/phpstan-webmozart-assert": "^2.0",
111111
"povils/phpmnd": "^3.6",
112+
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
112113
"rector/rector": "^2.2",
113114
"rector/swiss-knife": "^2.3",
114115
"rector/type-perfect": "^2.1",
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2021-2025 guanguans<[email protected]>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*
11+
* @see https://github.com/guanguans/notify
12+
*/
13+
14+
namespace Guanguans\Notify\Foundation\Caches;
15+
16+
use Psr\SimpleCache\CacheInterface;
17+
18+
/**
19+
* @see https://github.com/pestphp/pest-plugin-mutate/blob/4.x/src/Cache/FileStore.php
20+
*/
21+
class FileCache implements CacheInterface
22+
{
23+
/** @var string */
24+
private const CACHE_FOLDER_NAME = 'pest-mutate-cache';
25+
26+
/** @readonly */
27+
private string $directory;
28+
29+
public function __construct(?string $directory = null)
30+
{
31+
$this->directory = $directory ?? (sys_get_temp_dir().\DIRECTORY_SEPARATOR.self::CACHE_FOLDER_NAME); // @pest-mutate-ignore
32+
33+
if (!is_dir($this->directory)) { // @pest-mutate-ignore
34+
mkdir($this->directory, recursive: true);
35+
}
36+
}
37+
38+
public function get(string $key, mixed $default = null): mixed
39+
{
40+
return $this->getPayload($key) ?? $default;
41+
}
42+
43+
public function set(string $key, mixed $value, null|\DateInterval|int $ttl = null): bool
44+
{
45+
$payload = serialize($value);
46+
47+
$expire = $this->expiration($ttl);
48+
49+
$content = $expire.$payload;
50+
51+
$result = file_put_contents($this->filePathFromKey($key), $content);
52+
53+
return false !== $result; // @pest-mutate-ignore FalseToTrue
54+
}
55+
56+
public function delete(string $key): bool
57+
{
58+
return unlink($this->filePathFromKey($key));
59+
}
60+
61+
public function clear(): bool
62+
{
63+
foreach ((array) glob($this->directory.\DIRECTORY_SEPARATOR.'*') as $fileName) {
64+
// @pest-mutate-ignore
65+
if (false === $fileName) {
66+
continue;
67+
}
68+
69+
if (!str_starts_with(basename($fileName), 'cache-')) {
70+
continue;
71+
}
72+
73+
// @pest-mutate-ignore
74+
unlink($fileName);
75+
}
76+
77+
return true;
78+
}
79+
80+
public function getMultiple(iterable $keys, mixed $default = null): iterable
81+
{
82+
$result = [];
83+
84+
foreach ($keys as $key) {
85+
$result[$key] = $this->get($key, $default);
86+
}
87+
88+
return $result;
89+
}
90+
91+
/**
92+
* @param iterable<string, mixed> $values
93+
*/
94+
public function setMultiple(iterable $values, null|\DateInterval|int $ttl = null): bool
95+
{
96+
$result = true;
97+
98+
foreach ($values as $key => $value) {
99+
if (!$this->set($key, $value, $ttl)) {
100+
$result = false;
101+
}
102+
}
103+
104+
return $result;
105+
}
106+
107+
/**
108+
* @param iterable<string> $keys
109+
*/
110+
public function deleteMultiple(iterable $keys): bool
111+
{
112+
$result = true;
113+
114+
foreach ($keys as $key) {
115+
if (!$this->delete($key)) {
116+
$result = false;
117+
}
118+
}
119+
120+
return $result;
121+
}
122+
123+
public function has(string $key): bool
124+
{
125+
return file_exists($this->filePathFromKey($key));
126+
}
127+
128+
public function directory(): string
129+
{
130+
return $this->directory;
131+
}
132+
133+
protected function emptyPayload(): mixed
134+
{
135+
return null;
136+
}
137+
138+
private function filePathFromKey(string $key): string
139+
{
140+
return $this->directory.\DIRECTORY_SEPARATOR.'cache-'.hash('md5', $key); // @pest-mutate-ignore
141+
}
142+
143+
private function expiration(null|\DateInterval|int $seconds): int
144+
{
145+
if ($seconds instanceof \DateInterval) {
146+
return (new \DateTimeImmutable)->add($seconds)->getTimestamp();
147+
}
148+
149+
$seconds ??= 0;
150+
151+
if (0 === $seconds) {
152+
return 9_999_999_999; // @pest-mutate-ignore
153+
}
154+
155+
return time() + $seconds;
156+
}
157+
158+
private function getPayload(string $key): mixed
159+
{
160+
if (!file_exists($this->filePathFromKey($key))) {
161+
return $this->emptyPayload();
162+
}
163+
164+
$content = file_get_contents($this->filePathFromKey($key));
165+
166+
if (false === $content) { // @pest-mutate-ignore
167+
return $this->emptyPayload();
168+
}
169+
170+
try {
171+
$expire = (int) substr(
172+
$content,
173+
0,
174+
10
175+
);
176+
} catch (\Exception) {
177+
$this->delete($key);
178+
179+
return $this->emptyPayload();
180+
}
181+
182+
if (time() >= $expire) {
183+
$this->delete($key);
184+
185+
return $this->emptyPayload();
186+
}
187+
188+
try {
189+
$data = unserialize(substr($content, 10));
190+
} catch (\Exception) {
191+
$this->delete($key);
192+
193+
return $this->emptyPayload();
194+
}
195+
196+
return $data;
197+
}
198+
}

0 commit comments

Comments
 (0)