Passed
Pull Request — main (#63)
by Sílvio
03:01
created

FileCacheStore::getAll()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 3
nop 1
dl 0
loc 21
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
namespace Silviooosilva\CacheerPhp\CacheStore;
4
5
use Silviooosilva\CacheerPhp\Interface\CacheerInterface;
6
use Silviooosilva\CacheerPhp\CacheStore\CacheManager\FileCacheManager;
7
use Silviooosilva\CacheerPhp\CacheStore\CacheManager\FileCacheFlusher;
8
use Silviooosilva\CacheerPhp\Exceptions\CacheFileException;
9
use Silviooosilva\CacheerPhp\Helpers\CacheFileHelper;
10
use Silviooosilva\CacheerPhp\Utils\CacheLogger;
11
use Silviooosilva\CacheerPhp\CacheStore\Support\FileCachePathBuilder;
12
use Silviooosilva\CacheerPhp\CacheStore\Support\FileCacheBatchProcessor;
13
14
/**
15
 * Class FileCacheStore
16
 * @author Sílvio Silva <https://github.com/silviooosilva>
17
 * @package Silviooosilva\CacheerPhp
18
 */
19
class FileCacheStore implements CacheerInterface
20
{
21
    /**
22
     * @param string $cacheDir
23
     */
24
    private string $cacheDir;
25
26
    /**
27
     * @param array $options
28
     */
29
    private array $options = [];
30
31
    /**
32
     * @param string $message
33
     */
34
    private string $message = '';
35
36
    /**
37
     * @var FileCachePathBuilder
38
     */
39
    private FileCachePathBuilder $pathBuilder;
40
    
41
    /**
42
     * @var FileCacheBatchProcessor
43
     */
44
    private FileCacheBatchProcessor $batchProcessor;
45
    /**
46
     * @param integer $defaultTTL
47
     */
48
    private int $defaultTTL = 3600; // 1 hour default TTL
49
50
    /**
51
     * @param boolean $success
52
     */
53
    private bool $success = false;
54
55
56
    /**
57
    * @var CacheLogger
58
    */
59
    private $logger = null;
60
61
    /**
62
    * @var FileCacheManager
63
    */
64
    private FileCacheManager $fileManager;
65
66
    /**
67
    * @var FileCacheFlusher
68
    */
69
    private FileCacheFlusher $flusher;
70
71
72
    /**
73
     * FileCacheStore constructor.
74
     * @param array $options
75
     * @throws CacheFileException
76
     */
77
    public function __construct(array $options = [])
78
    {
79
        $this->validateOptions($options);
80
        $this->fileManager = new FileCacheManager();
81
        $this->initializeCacheDir($options['cacheDir']);
82
        $this->pathBuilder = new FileCachePathBuilder($this->fileManager, $this->cacheDir);
83
        $this->batchProcessor = new FileCacheBatchProcessor($this);
84
        $this->flusher = new FileCacheFlusher($this->fileManager, $this->cacheDir);
85
        $this->defaultTTL = $this->getExpirationTime($options);
86
        $this->flusher->handleAutoFlush($options);
87
        $this->logger = new CacheLogger($options['loggerPath']);
88
    }
89
90
    /**
91
     * Returns the path for a tag index file.
92
     * @param string $tag
93
     * @return string
94
     */
95
    private function getTagIndexPath(string $tag): string
96
    {
97
        $tagDir = rtrim($this->cacheDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '_tags';
98
        if (!$this->fileManager->directoryExists($tagDir)) {
99
            $this->fileManager->createDirectory($tagDir);
100
        }
101
        return $tagDir . DIRECTORY_SEPARATOR . $tag . '.json';
102
    }
103
104
    /**
105
     * Appends data to an existing cache item.
106
     *
107
     * @param string $cacheKey
108
     * @param mixed $cacheData
109
     * @param string $namespace
110
     * @return string|void
111
     * @throws CacheFileException
112
     */
113
    public function appendCache(string $cacheKey, mixed $cacheData, string $namespace = '')
114
    {
115
        $currentCacheFileData = $this->getCache($cacheKey, $namespace);
116
117
        if (!$this->isSuccess()) {
118
            return $this->getMessage();
119
        }
120
121
        $mergedCacheData = CacheFileHelper::arrayIdentifier($currentCacheFileData, $cacheData);
122
123
124
        $this->putCache($cacheKey, $mergedCacheData, $namespace);
125
        if ($this->isSuccess()) {
126
            $this->setMessage("Cache updated successfully", true);
127
            $this->logger->debug("{$this->getMessage()} from file driver.");
128
        }
129
    }
130
131
    /**
132
     * Builds the cache file path based on the cache key and namespace.
133
     * 
134
     * @param string $cacheKey
135
     * @param string $namespace
136
     * @return string
137
     */
138
    private function buildCacheFilePath(string $cacheKey, string $namespace): string
139
    {
140
        return $this->pathBuilder->build($cacheKey, $namespace);
141
    }
142
143
    /**
144
     * Clears a specific cache item.
145
     *
146
     * @param string $cacheKey
147
     * @param string $namespace
148
     * @return bool
149
     * @throws CacheFileException
150
     */
151
    public function clearCache(string $cacheKey, string $namespace = ''): void
152
    {
153
        $cacheFile = $this->buildCacheFilePath($cacheKey, $namespace);
154
        if ($this->fileManager->readFile($cacheFile)) {
155
            $this->fileManager->removeFile($cacheFile);
156
            $this->setMessage("Cache file deleted successfully!", true);
157
        } else {
158
            $this->setMessage("Cache file does not exist!", false);
159
        }
160
        $this->logger->debug("{$this->getMessage()} from file driver.");
161
    }
162
163
    /**
164
     * Flushes all cache items.
165
     * 
166
     * @return void
167
     */
168
    public function flushCache(): void
169
    {
170
        $this->flusher->flushCache();
171
    }
172
173
    /**
174
     * Associates one or more keys to a tag.
175
     *
176
     * @param string $tag
177
     * @param string ...$keys
178
     * @return bool
179
     */
180
    public function tag(string $tag, string ...$keys): bool
181
    {
182
        $path = $this->getTagIndexPath($tag);
183
        $current = [];
184
        if ($this->fileManager->fileExists($path)) {
185
            $json = $this->fileManager->readFile($path);
186
            $decoded = json_decode($json, true);
187
            if (is_array($decoded)) {
188
                $current = $decoded;
189
            }
190
        }
191
        foreach ($keys as $key) {
192
            // Store either raw key or "namespace:key"
193
            $current[$key] = true;
194
        }
195
        $this->fileManager->writeFile($path, json_encode($current));
196
        $this->setMessage("Tagged successfully", true);
197
        $this->logger->debug("{$this->getMessage()} from file driver.");
198
        return true;
199
    }
200
201
    /**
202
     * Flush all keys associated with a tag.
203
     *
204
     * @param string $tag
205
     * @return void
206
     */
207
    public function flushTag(string $tag): void
208
    {
209
        $path = $this->getTagIndexPath($tag);
210
        $current = [];
211
        if ($this->fileManager->fileExists($path)) {
212
            $json = $this->fileManager->readFile($path);
213
            $current = json_decode($json, true) ?: [];
214
        }
215
        foreach (array_keys($current) as $key) {
216
            if (str_contains($key, ':')) {
217
                [$np, $k] = explode(':', $key, 2);
218
                $this->clearCache($k, $np);
219
            } else {
220
                $this->clearCache($key, '');
221
            }
222
        }
223
        if ($this->fileManager->fileExists($path)) {
224
            $this->fileManager->removeFile($path);
225
        }
226
        $this->setMessage("Tag flushed successfully", true);
227
        $this->logger->debug("{$this->getMessage()} from file driver.");
228
    }
229
230
    /**
231
     * Retrieves the expiration time from options or uses the default TTL.
232
     *
233
     * @param array $options
234
     * @return integer
235
     * @throws CacheFileException
236
     */
237
    private function getExpirationTime(array $options): int
238
    {
239
        return isset($options['expirationTime'])
240
            ? CacheFileHelper::convertExpirationToSeconds($options['expirationTime'])
241
            : $this->defaultTTL;
242
    }
243
244
    /**
245
     * Retrieves a message indicating the status of the last operation.
246
     * 
247
     * @return string
248
     */
249
    public function getMessage(): string
250
    {
251
        return $this->message;
252
    }
253
254
    /**
255
     * Retrieves a single cache item.
256
     *
257
     * @param string $cacheKey
258
     * @param string $namespace
259
     * @param string|int $ttl
260
     * @return mixed
261
     * @throws CacheFileException return string|void
262
     */
263
    public function getCache(string $cacheKey, string $namespace = '', string|int $ttl = 3600): mixed
264
    {
265
       
266
        $ttl = CacheFileHelper::ttl($ttl, $this->defaultTTL);
267
        $cacheFile = $this->buildCacheFilePath($cacheKey, $namespace);
268
        if ($this->isCacheValid($cacheFile, $ttl)) {
269
            $cacheData = $this->fileManager->serialize($this->fileManager->readFile($cacheFile), false);
270
271
            $this->setMessage("Cache retrieved successfully", true);
272
            $this->logger->debug("{$this->getMessage()} from file driver.");
273
            return $cacheData;
274
        }
275
276
        $this->setMessage("cacheFile not found, does not exists or expired", false);
277
        $this->logger->info("{$this->getMessage()} from file driver.");
278
        return null;
279
    }
280
281
    /**
282
     * @param string $namespace
283
     * @return array
284
     * @throws CacheFileException
285
     */
286
    public function getAll(string $namespace = ''): array
287
    {
288
        $cacheDir = $this->getNamespaceCacheDir($namespace);
289
290
        if (!$this->fileManager->directoryExists($cacheDir)) {
291
            $this->setMessage("Cache directory does not exist", false);
292
            $this->logger->info("{$this->getMessage()} from file driver.");
293
            return [];
294
        }
295
296
        $results = $this->getAllCacheFiles($cacheDir);
297
298
        if (!empty($results)) {
299
            $this->setMessage("Cache retrieved successfully", true);
300
            $this->logger->debug("{$this->getMessage()} from file driver.");
301
            return $results;
302
        }
303
304
        $this->setMessage("No cache data found for the provided namespace", false);
305
        $this->logger->info("{$this->getMessage()} from file driver.");
306
        return [];
307
    }
308
309
    /**
310
     * Return the cache directory for the given namespace.
311
     * 
312
     * @param string $namespace
313
     * @return string
314
     */
315
    private function getNamespaceCacheDir(string $namespace): string
316
    {
317
        return $this->pathBuilder->namespaceDir($namespace);
318
    }
319
320
    /**
321
     * Return all valid cache files from the specified directory.
322
     *
323
     * @param string $cacheDir
324
     * @return array
325
     * @throws CacheFileException
326
     */
327
    private function getAllCacheFiles(string $cacheDir): array
328
    {
329
        $files = $this->fileManager->getFilesInDirectory($cacheDir);
330
        $results = [];
331
332
        foreach ($files as $file) {
333
            if (pathinfo($file, PATHINFO_EXTENSION) === 'cache') {
334
                $cacheKey = basename($file, '.cache');
335
                $cacheData = $this->fileManager->serialize($this->fileManager->readFile($file), false);
336
                $results[$cacheKey] = $cacheData;
337
            }
338
        }
339
        return $results;
340
    }
341
342
    /**
343
     * Gets the cache data for multiple keys.
344
     *
345
     * @param array $cacheKeys
346
     * @param string $namespace
347
     * @param string|int $ttl
348
     * @return array
349
     * @throws CacheFileException
350
     */
351
    public function getMany(array $cacheKeys, string $namespace = '', string|int $ttl = 3600): array
352
    {
353
        $ttl = CacheFileHelper::ttl($ttl, $this->defaultTTL);
354
        $results = [];
355
356
        foreach ($cacheKeys as $cacheKey) {
357
            $cacheData = $this->getCache($cacheKey, $namespace, $ttl);
358
            if ($this->isSuccess()) {
359
                $results[$cacheKey] = $cacheData;
360
            } else {
361
                $results[$cacheKey] = null;
362
            }
363
        }
364
365
        return $results;
366
    }
367
368
    /**
369
     * Stores multiple cache items in batches.
370
     * 
371
     * @param array   $items
372
     * @param string  $namespace
373
     * @param integer $batchSize
374
     * @return void
375
     */
376
    public function putMany(array $items, string $namespace = '', int $batchSize = 100): void
377
    {
378
        $processedCount = 0;
379
        $itemCount = count($items);
380
381
        while ($processedCount < $itemCount) {
382
            $batchItems = array_slice($items, $processedCount, $batchSize);
383
            $this->processBatchItems($batchItems, $namespace);
384
            $processedCount += count($batchItems);
385
        }
386
    }
387
388
    /**
389
     * Stores an item in the cache with a specific TTL.
390
     *
391
     * @param string $cacheKey
392
     * @param mixed $cacheData
393
     * @param string $namespace
394
     * @param string|int $ttl
395
     * @return void
396
     * @throws CacheFileException
397
     */
398
    public function putCache(string $cacheKey, mixed $cacheData, string $namespace = '', string|int $ttl = 3600): void
399
    {
400
        $cacheFile = $this->buildCacheFilePath($cacheKey, $namespace);
401
        $data = $this->fileManager->serialize($cacheData);
402
403
        $this->fileManager->writeFile($cacheFile, $data);
404
        $this->setMessage("Cache file created successfully", true);
405
406
    $this->logger->debug("{$this->getMessage()} from file driver.");
407
}
408
409
    /**
410
     * Checks if a cache key exists.
411
     *
412
     * @param string $cacheKey
413
     * @param string $namespace
414
     * @return bool
415
     * @throws CacheFileException
416
     */
417
    public function has(string $cacheKey, string $namespace = ''): bool
418
    {
419
        $this->getCache($cacheKey, $namespace);
420
421
        if ($this->isSuccess()) {
422
            $this->setMessage("Cache key: {$cacheKey} exists and it's available! from file driver", true);
423
            return true;
424
        }
425
426
        $this->setMessage("Cache key: {$cacheKey} does not exists or it's expired! from file driver", false);
427
        return false;
428
    }
429
430
    /**
431
     * Renews the cache for a specific key.
432
     *
433
     * @param string $cacheKey
434
     * @param string|int $ttl
435
     * @param string $namespace
436
     * @return void
437
     * @throws CacheFileException
438
     */
439
    public function renewCache(string $cacheKey, string|int $ttl, string $namespace = ''): void
440
    {
441
        $cacheData = $this->getCache($cacheKey, $namespace);
442
        if ($cacheData) {
443
            $this->putCache($cacheKey, $cacheData, $namespace, $ttl);
444
            $this->setMessage("Cache with key {$cacheKey} renewed successfully", true);
445
            $this->logger->debug("{$this->getMessage()} from file driver.");
446
            return;
447
        }
448
        $this->setMessage("Failed to renew Cache with key {$cacheKey}", false);
449
        $this->logger->debug("{$this->getMessage()} from file driver.");
450
    }
451
452
    /**
453
     * Processes a batch of cache items.
454
     * 
455
     * @param array  $batchItems
456
     * @param string $namespace
457
     * @return void
458
     */
459
    private function processBatchItems(array $batchItems, string $namespace): void
460
    {
461
        $this->batchProcessor->process($batchItems, $namespace);
462
    }
463
464
    /**
465
     * Checks if the last operation was successful.
466
     * 
467
     * @return boolean
468
     */
469
    public function isSuccess(): bool
470
    {
471
        return $this->success;
472
    }
473
474
    /**
475
     * Sets a message indicating the status of the last operation.
476
     * 
477
     * @param string  $message
478
     * @param boolean $success
479
     * @return void
480
     */
481
    private function setMessage(string $message, bool $success): void
482
    {
483
        $this->message = $message;
484
        $this->success = $success;
485
    }
486
487
    /**
488
     * Validates the options provided to the cache store.
489
     *
490
     * @param array $options
491
     * @return void
492
     * @throws CacheFileException
493
     */
494
    private function validateOptions(array $options): void
495
    {
496
        if (!isset($options['cacheDir']) && $options['drive'] === 'file') {
497
            $this->logger->debug("The 'cacheDir' option is required from file driver.");
498
            throw CacheFileException::create("The 'cacheDir' option is required.");
499
        }
500
        $this->options = $options;
501
    }
502
503
    /**
504
     * Initializes the cache directory.
505
     *
506
     * @param string $cacheDir
507
     * @return void
508
     * @throws CacheFileException
509
     */
510
    private function initializeCacheDir(string $cacheDir): void
511
    {
512
        $this->cacheDir = realpath($cacheDir) ?: "";
513
        $this->fileManager->createDirectory($cacheDir);
514
    }
515
516
517
518
    /**
519
     * Checks if the cache file is valid based on its existence and modification time.
520
     * 
521
     * @param string  $cacheFile
522
     * @param integer $ttl
523
     * @return boolean
524
     */
525
    private function isCacheValid(string $cacheFile, int $ttl): bool
526
    {
527
        return file_exists($cacheFile) && (filemtime($cacheFile) > (time() - $ttl));
528
    }
529
}
530