Skip to content

Commit c9ea66f

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 c9ea66f

File tree

11 files changed

+525
-6
lines changed

11 files changed

+525
-6
lines changed

baselines/disallowed.function.neon

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# total 7 errors
1+
# total 8 errors
22

33
parameters:
44
ignoreErrors:
@@ -12,6 +12,11 @@ parameters:
1212
count: 1
1313
path: ../src/Foundation/Authenticators/WsseAuthenticator.php
1414

15+
-
16+
message: '#^Calling hash\(\) is forbidden, use hash\(\) with at least SHA\-256 for secure hash, or password_hash\(\) for passwords\.$#'
17+
count: 1
18+
path: ../src/Foundation/Caches/FileCache.php
19+
1520
-
1621
message: '#^Calling var_dump\(\) is forbidden, use some logger instead\.$#'
1722
count: 1

baselines/loader.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# total 16 errors
1+
# total 17 errors
22

33
includes:
44
- assign.propertyType.neon

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",

rector-php82.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,9 @@
5151
'token',
5252
'webHook',
5353
],
54+
])
55+
->withSkip([
56+
AddSensitiveParameterAttributeRector::class => [
57+
__DIR__.'/src/Foundation/Caches/',
58+
],
5459
]);
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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+
* @throws \Psr\SimpleCache\InvalidArgumentException
95+
*/
96+
public function setMultiple(iterable $values, null|\DateInterval|int $ttl = null): bool
97+
{
98+
$result = true;
99+
100+
foreach ($values as $key => $value) {
101+
if (!$this->set($key, $value, $ttl)) {
102+
$result = false;
103+
}
104+
}
105+
106+
return $result;
107+
}
108+
109+
/**
110+
* @param iterable<string> $keys
111+
*
112+
* @throws \Psr\SimpleCache\InvalidArgumentException
113+
*/
114+
public function deleteMultiple(iterable $keys): bool
115+
{
116+
$result = true;
117+
118+
foreach ($keys as $key) {
119+
if (!$this->delete($key)) {
120+
$result = false;
121+
}
122+
}
123+
124+
return $result;
125+
}
126+
127+
public function has(string $key): bool
128+
{
129+
return file_exists($this->filePathFromKey($key));
130+
}
131+
132+
public function directory(): string
133+
{
134+
return $this->directory;
135+
}
136+
137+
protected function emptyPayload(): mixed
138+
{
139+
return null;
140+
}
141+
142+
private function filePathFromKey(string $key): string
143+
{
144+
return $this->directory.\DIRECTORY_SEPARATOR.'cache-'.hash('md5', $key); // @pest-mutate-ignore
145+
}
146+
147+
private function expiration(null|\DateInterval|int $seconds): int
148+
{
149+
if ($seconds instanceof \DateInterval) {
150+
return (new \DateTimeImmutable)->add($seconds)->getTimestamp();
151+
}
152+
153+
$seconds ??= 0;
154+
155+
if (0 === $seconds) {
156+
return 9_999_999_999; // @pest-mutate-ignore
157+
}
158+
159+
return time() + $seconds;
160+
}
161+
162+
/**
163+
* @throws \Psr\SimpleCache\InvalidArgumentException
164+
*/
165+
private function getPayload(string $key): mixed
166+
{
167+
if (!file_exists($this->filePathFromKey($key))) {
168+
return $this->emptyPayload();
169+
}
170+
171+
$content = file_get_contents($this->filePathFromKey($key));
172+
173+
if (false === $content) { // @pest-mutate-ignore
174+
return $this->emptyPayload();
175+
}
176+
177+
try {
178+
$expire = (int) substr(
179+
$content,
180+
0,
181+
10
182+
);
183+
} catch (\Exception) {
184+
$this->delete($key);
185+
186+
return $this->emptyPayload();
187+
}
188+
189+
if (time() >= $expire) {
190+
$this->delete($key);
191+
192+
return $this->emptyPayload();
193+
}
194+
195+
try {
196+
$data = unserialize(substr($content, 10));
197+
} catch (\Exception) {
198+
$this->delete($key);
199+
200+
return $this->emptyPayload();
201+
}
202+
203+
return $data;
204+
}
205+
}

0 commit comments

Comments
 (0)