cache.php

main 334 lines · 9.2 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2
3class Cache
4{
5 protected string $_file;
6 protected string $_key;
7 protected string $_prefix;
8 protected int $_lifespan = 0;
9 protected mixed $_content = null;
10 protected bool $_memory = false;
11 protected bool $_canMemory = false;
12
13 private const FORMAT = 'ZNOTEX_CACHE_V1:';
14 public const EXT = '.cache';
15
16 public function __construct(string $file)
17 {
18 $cfg = function_exists('config')
19 ? config('cache')
20 : ($GLOBALS['config']['cache'] ?? array());
21
22 if (!is_array($cfg)) {
23 $cfg = array();
24 }
25
26 $this->_lifespan = max(0, (int)($cfg['lifespan'] ?? 60));
27 $this->_prefix = self::normalizePrefix((string)($cfg['prefix'] ?? 'znote_'));
28 $this->_canMemory = self::memoryAvailable();
29 $this->_memory = !empty($cfg['memory']) && $this->_canMemory;
30 $this->_file = $file . self::EXT;
31 $logicalName = str_replace('\\', '/', $this->_file);
32 $this->_key = $this->_prefix . 'cache:' . hash('sha256', $logicalName);
33 }
34
35 public static function memoryAvailable(): bool
36 {
37 if (!function_exists('apcu_fetch') || !function_exists('apcu_store')) {
38 return false;
39 }
40
41 if (function_exists('apcu_enabled')) {
42 return apcu_enabled();
43 }
44
45 if (PHP_SAPI === 'cli' && !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOL)) {
46 return false;
47 }
48
49 return filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOL);
50 }
51
52 public static function configuredPrefix(): string
53 {
54 $cfg = function_exists('config')
55 ? config('cache')
56 : ($GLOBALS['config']['cache'] ?? array());
57
58 return self::normalizePrefix(is_array($cfg) ? (string)($cfg['prefix'] ?? 'znote_') : 'znote_');
59 }
60
61 private static function normalizePrefix(string $prefix): string
62 {
63 $prefix = preg_replace('/[^a-zA-Z0-9_.:-]/', '_', trim($prefix)) ?? '';
64 return substr($prefix !== '' ? $prefix : 'znote_', 0, 64);
65 }
66
67 public function setExpiration(int $span): void
68 {
69 $this->_lifespan = max(0, $span);
70 }
71
72 public function useMemory(bool $bool): bool
73 {
74 $this->_memory = $bool && $this->_canMemory;
75 return $this->_memory;
76 }
77
78 public function driver(): string
79 {
80 return $this->_memory ? 'apcu' : 'file';
81 }
82
83 public function setContent(mixed $content): void
84 {
85 $this->_content = $content;
86 }
87
88 public function hasExpired(): bool
89 {
90 if ($this->_memory) {
91 return !apcu_exists($this->_key);
92 }
93
94 if (!is_file($this->_file)) {
95 return true;
96 }
97
98 if ($this->_lifespan === 0) {
99 return false;
100 }
101
102 $modified = filemtime($this->_file);
103 return $modified === false || time() >= $modified + $this->_lifespan;
104 }
105
106 public function remainingTime(): int
107 {
108 if ($this->hasExpired()) {
109 return 0;
110 }
111
112 if ($this->_lifespan === 0) {
113 return PHP_INT_MAX;
114 }
115
116 if ($this->_memory) {
117 $success = false;
118 $payload = apcu_fetch($this->_key, $success);
119 if (!$success) {
120 return 0;
121 }
122
123 $envelope = $this->decodeEnvelope($payload);
124 return is_array($envelope)
125 ? max(0, (int)$envelope['expires'] - time())
126 : 0;
127 }
128
129 $modified = filemtime($this->_file);
130 return $modified === false ? 0 : max(0, ($modified + $this->_lifespan) - time());
131 }
132
133 public function save(): bool
134 {
135 $payload = $this->encodeContent();
136 if ($payload === false) {
137 return false;
138 }
139
140 if ($this->_memory) {
141 return apcu_store($this->_key, $payload, $this->_lifespan);
142 }
143
144 $directory = dirname($this->_file);
145 if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory)) {
146 return false;
147 }
148
149 $temporary = tempnam($directory, '.znote-cache-');
150 if ($temporary === false) {
151 return false;
152 }
153
154 $written = file_put_contents($temporary, $payload, LOCK_EX);
155 if ($written === false) {
156 @unlink($temporary);
157 return false;
158 }
159
160 if (@rename($temporary, $this->_file)) {
161 clearstatcache(true, $this->_file);
162 return true;
163 }
164
165 $saved = file_put_contents($this->_file, $payload, LOCK_EX) !== false;
166 @unlink($temporary);
167 clearstatcache(true, $this->_file);
168 return $saved;
169 }
170
171 public function load(): mixed
172 {
173 if ($this->_memory) {
174 $success = false;
175 $payload = apcu_fetch($this->_key, $success);
176 return $success ? $this->decodeContent($payload) : false;
177 }
178
179 if (!is_file($this->_file)) {
180 return false;
181 }
182
183 $handle = @fopen($this->_file, 'rb');
184 if ($handle === false) {
185 return false;
186 }
187
188 $payload = false;
189 if (flock($handle, LOCK_SH)) {
190 $payload = stream_get_contents($handle);
191 flock($handle, LOCK_UN);
192 }
193 fclose($handle);
194
195 return $payload === false || $payload === '' ? false : $this->decodeContent($payload);
196 }
197
198 public function delete(): bool
199 {
200 if ($this->_memory) {
201 return !apcu_exists($this->_key) || apcu_delete($this->_key);
202 }
203
204 if (!is_file($this->_file)) {
205 return true;
206 }
207
208 $deleted = @unlink($this->_file);
209 clearstatcache(true, $this->_file);
210 return $deleted;
211 }
212
213 private function encodeContent(): string|false
214 {
215 $envelope = array(
216 'version' => 1,
217 'expires' => $this->_lifespan === 0 ? 0 : time() + $this->_lifespan,
218 'value' => $this->_content,
219 );
220
221 try {
222 return self::FORMAT . base64_encode(serialize($envelope));
223 } catch (Throwable $error) {
224 return false;
225 }
226 }
227
228 private function decodeEnvelope(mixed $payload): array|false
229 {
230 if (!is_string($payload) || !str_starts_with($payload, self::FORMAT)) {
231 return false;
232 }
233
234 $decoded = base64_decode(substr($payload, strlen(self::FORMAT)), true);
235 if ($decoded === false) {
236 return false;
237 }
238
239 try {
240 $envelope = @unserialize($decoded, array('allowed_classes' => false));
241 } catch (Throwable $error) {
242 return false;
243 }
244
245 return is_array($envelope)
246 && ($envelope['version'] ?? null) === 1
247 && array_key_exists('value', $envelope)
248 ? $envelope
249 : false;
250 }
251
252 private function decodeContent(mixed $payload): mixed
253 {
254 $envelope = $this->decodeEnvelope($payload);
255 if ($envelope !== false) {
256 return $envelope['value'];
257 }
258
259 if (!is_string($payload) || $payload === '') {
260 return $payload;
261 }
262
263 $json = json_decode($payload, true);
264 return json_last_error() === JSON_ERROR_NONE ? $json : $payload;
265 }
266}
267
268function znote_cache_stats(): array
269{
270 $files = 0;
271 $bytes = 0;
272 $root = realpath('engine/cache');
273
274 if ($root !== false && is_dir($root)) {
275 $iterator = new RecursiveIteratorIterator(
276 new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)
277 );
278
279 foreach ($iterator as $entry) {
280 if ($entry->isFile() && str_ends_with($entry->getFilename(), Cache::EXT)) {
281 $files++;
282 $bytes += $entry->getSize();
283 }
284 }
285 }
286
287 $cfg = function_exists('config')
288 ? config('cache')
289 : ($GLOBALS['config']['cache'] ?? array());
290 $requestedMemory = is_array($cfg) && !empty($cfg['memory']);
291
292 return array(
293 'driver' => $requestedMemory && Cache::memoryAvailable() ? 'APCu' : 'Files',
294 'requested_memory' => $requestedMemory,
295 'apcu_available' => Cache::memoryAvailable(),
296 'files' => $files,
297 'bytes' => $bytes,
298 'prefix' => Cache::configuredPrefix(),
299 );
300}
301
302function znote_cache_flush(): int
303{
304 $removed = 0;
305 $root = realpath('engine/cache');
306
307 if ($root !== false && is_dir($root)) {
308 $iterator = new RecursiveIteratorIterator(
309 new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
310 RecursiveIteratorIterator::CHILD_FIRST
311 );
312
313 foreach ($iterator as $entry) {
314 if ($entry->isFile() && str_ends_with($entry->getFilename(), Cache::EXT) && @unlink($entry->getPathname())) {
315 $removed++;
316 }
317 }
318 }
319
320 if (Cache::memoryAvailable() && function_exists('apcu_cache_info')) {
321 $prefix = Cache::configuredPrefix() . 'cache:';
322 $info = apcu_cache_info(false);
323 foreach (($info['cache_list'] ?? array()) as $item) {
324 $key = (string)($item['info'] ?? '');
325 $legacyKey = str_replace('\\', '/', $key);
326 if ((str_starts_with($key, $prefix) || str_starts_with($legacyKey, 'engine/cache/')) && apcu_delete($key)) {
327 $removed++;
328 }
329 }
330 }
331
332 return $removed;
333}
334
Top