Passed
Push — main ( a8c6a0...1905a9 )
by Sílvio
01:03 queued 16s
created

FileCacheStore   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 459
Duplicated Lines 0 %

Importance

Changes 6
Bugs 2 Features 1
Metric Value
eloc 134
dl 0
loc 459
rs 8.48
c 6
b 2
f 1
wmc 49

24 Methods

Rating   Name   Duplication   Size   Complexity  
A validateOptions() 0 7 3
A putMany() 0 9 2
A handleAutoFlush() 0 4 2
A getExpirationTime() 0 5 2
A appendCache() 0 15 3
A clearCache() 0 10 2
A initializeCacheDir() 0 4 2
A flushCache() 0 4 1
A getAll() 0 21 3
A putCache() 0 9 1
A buildCacheFilePath() 0 10 3
A isCacheValid() 0 3 2
A getAllCacheFiles() 0 13 3
A getCache() 0 15 2
A renewCache() 0 11 2
A setMessage() 0 4 1
A getMany() 0 15 3
A has() 0 8 2
A isSuccess() 0 3 1
A processBatchItems() 0 8 2
A getNamespaceCacheDir() 0 4 2
A getMessage() 0 3 1
A scheduleFlush() 0 14 3
A __construct() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like FileCacheStore often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FileCacheStore, and based on these observations, apply Extract Interface, too.

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\Exceptions\CacheFileException;
8
use Silviooosilva\CacheerPhp\Helpers\CacheFileHelper;
9
use Silviooosilva\CacheerPhp\Utils\CacheLogger;
10
11
/**
12
 * Class FileCacheStore
13
 * @author Sílvio Silva <https://github.com/silviooosilva>
14
 * @package Silviooosilva\CacheerPhp
15
 */
16
class FileCacheStore implements CacheerInterface
17
{
18
    /**
19
     * @param string $cacheDir
20
     */
21
    private string $cacheDir;
22
23
    /**
24
     * @param array $options
25
     */
26
    private array $options = [];
27
28
    /**
29
     * @param string $message
30
     */
31
    private string $message = '';
32
33
    /**
34
     * @param integer $defaultTTL
35
     */
36
    private int $defaultTTL = 3600; // 1 hour default TTL
37
38
    /**
39
     * @param boolean $success
40
     */
41
    private bool $success = false;
42
43
    /**
44
     * @param string $lastFlushTimeFile
45
     */
46
    private string $lastFlushTimeFile;
47
48
    /**
49
    * @var CacheLogger
50
    */
51
    private $logger = null;
52
53
    /**
54
    * @var FileCacheManager
55
    */
56
    private FileCacheManager $fileManager;
57
58
59
    /**
60
     * FileCacheStore constructor.
61
     * @param array $options
62
     */
63
    public function __construct(array $options = [])
64
    {
65
        $this->validateOptions($options);
66
        $this->fileManager = new FileCacheManager();
67
        $this->initializeCacheDir($options['cacheDir']);
68
        $this->defaultTTL = $this->getExpirationTime($options);
69
        $this->lastFlushTimeFile = "{$this->cacheDir}/last_flush_time";
70
        $this->handleAutoFlush($options);
71
        $this->logger = new CacheLogger($options['loggerPath']);
72
    }
73
74
    /**
75
     * Appends data to an existing cache item.
76
     * 
77
     * @param string $cacheKey
78
     * @param mixed  $cacheData
79
     * @param string $namespace
80
     * @return void
81
     */
82
    public function appendCache(string $cacheKey, mixed $cacheData, string $namespace = '')
83
    {
84
        $currentCacheFileData = $this->getCache($cacheKey, $namespace);
85
86
        if (!$this->isSuccess()) {
87
            return $this->getMessage();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getMessage() returns the type string which is incompatible with the documented return type void.
Loading history...
88
        }
89
90
        $mergedCacheData = CacheFileHelper::arrayIdentifier($currentCacheFileData, $cacheData);
91
92
93
        $this->putCache($cacheKey, $mergedCacheData, $namespace);
94
        if ($this->isSuccess()) {
95
            $this->setMessage("Cache updated successfully", true);
96
            $this->logger->debug("{$this->getMessage()} from file driver.");
97
        }
98
    }
99
100
    /**
101
     * Builds the cache file path based on the cache key and namespace.
102
     * 
103
     * @param string $cacheKey
104
     * @param string $namespace
105
     * @return string
106
     */
107
    private function buildCacheFilePath(string $cacheKey, string $namespace)
108
    {
109
        $namespace = $namespace ? md5($namespace) . '/' : '';
110
        $cacheDir = "{$this->cacheDir}/";
111
112
        if (!empty($namespace)) {
113
            $cacheDir = "{$this->cacheDir}/{$namespace}";
114
            $this->fileManager->createDirectory($cacheDir);
115
        }
116
        return $cacheDir . md5($cacheKey) . ".cache";
117
    }
118
119
    /**
120
     * Clears a specific cache item.
121
     * 
122
     * @param string $cacheKey
123
     * @param string $namespace
124
     * @return void
125
     */
126
    public function clearCache(string $cacheKey, string $namespace = '')
127
    {
128
        $cacheFile = $this->buildCacheFilePath($cacheKey, $namespace);
129
        if ($this->fileManager->readFile($cacheFile)) {
130
            $this->fileManager->removeFile($cacheFile);
131
            $this->setMessage("Cache file deleted successfully!", true);
132
        } else {
133
            $this->setMessage("Cache file does not exist!", false);
134
        }
135
        $this->logger->debug("{$this->getMessage()} from file driver.");
136
    }
137
138
    /**
139
     * Flushes all cache items.
140
     * 
141
     * @return void
142
     */
143
    public function flushCache()
144
    {
145
        $this->fileManager->clearDirectory($this->cacheDir);
146
        file_put_contents($this->lastFlushTimeFile, time());
147
    }
148
149
    /**
150
     * Retrieves the expiration time from options or uses the default TTL.
151
     * 
152
     * @param array $options
153
     * @return integer
154
     */
155
    private function getExpirationTime(array $options)
156
    {
157
        return isset($options['expirationTime'])
158
            ? CacheFileHelper::convertExpirationToSeconds($options['expirationTime'])
159
            : $this->defaultTTL;
160
    }
161
162
    /**
163
     * Retrieves a message indicating the status of the last operation.
164
     * 
165
     * @return string
166
     */
167
    public function getMessage()
168
    {
169
        return $this->message;
170
    }
171
172
    /**
173
     * Retrieves a single cache item.
174
     * 
175
     * @param string $cacheKey
176
     * @param string $namespace
177
     * @param string|int $ttl
178
     * @return string
179
     */
180
    public function getCache(string $cacheKey, string $namespace = '', string|int $ttl = 3600)
181
    {
182
       
183
        $ttl = CacheFileHelper::ttl($ttl, $this->defaultTTL);
184
        $cacheFile = $this->buildCacheFilePath($cacheKey, $namespace);
185
        if ($this->isCacheValid($cacheFile, $ttl)) {
186
            $cacheData = $this->fileManager->serialize($this->fileManager->readFile($cacheFile), false);
187
188
            $this->setMessage("Cache retrieved successfully", true);
189
            $this->logger->debug("{$this->getMessage()} from file driver.");
190
            return $cacheData;
191
        }
192
193
        $this->setMessage("cacheFile not found, does not exists or expired", false);
194
        $this->logger->info("{$this->getMessage()} from file driver.");
195
    }
196
197
    /**
198
     * @param string $namespace
199
     * @return array
200
     */
201
    public function getAll(string $namespace = '')
202
    {
203
        $cacheDir = $this->getNamespaceCacheDir($namespace);
204
205
        if (!$this->fileManager->directoryExists($cacheDir)) {
206
            $this->setMessage("Cache directory does not exist", false);
207
            $this->logger->info("{$this->getMessage()} from file driver.");
208
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the return type mandated by Silviooosilva\CacheerPhp...heerInterface::getAll() of Silviooosilva\CacheerPhp...face\CacheDataFormatter.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
209
        }
210
211
        $results = $this->getAllCacheFiles($cacheDir);
212
213
        if (!empty($results)) {
214
            $this->setMessage("Cache retrieved successfully", true);
215
            $this->logger->debug("{$this->getMessage()} from file driver.");
216
            return $results;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $results returns the type array which is incompatible with the return type mandated by Silviooosilva\CacheerPhp...heerInterface::getAll() of Silviooosilva\CacheerPhp...face\CacheDataFormatter.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
217
        }
218
219
        $this->setMessage("No cache data found for the provided namespace", false);
220
        $this->logger->info("{$this->getMessage()} from file driver.");
221
        return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the return type mandated by Silviooosilva\CacheerPhp...heerInterface::getAll() of Silviooosilva\CacheerPhp...face\CacheDataFormatter.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
222
    }
223
224
    /**
225
     * Return the cache directory for the given namespace.
226
     * 
227
     * @param string $namespace
228
     * @return string
229
     */
230
    private function getNamespaceCacheDir(string $namespace)
231
    {
232
        $namespace = $namespace ? md5($namespace) . '/' : '';
233
        return "{$this->cacheDir}/{$namespace}";
234
    }
235
236
    /**
237
     * Return all valid cache files from the specified directory.
238
     * 
239
     * @param string $cacheDir
240
     * @return array
241
     */
242
    private function getAllCacheFiles(string $cacheDir)
243
    {
244
        $files = $this->fileManager->getFilesInDirectory($cacheDir);
245
        $results = [];
246
247
        foreach ($files as $file) {
248
            if (pathinfo($file, PATHINFO_EXTENSION) === 'cache') {
249
                $cacheKey = basename($file, '.cache');
250
                $cacheData = $this->fileManager->serialize($this->fileManager->readFile($file), false);
251
                $results[$cacheKey] = $cacheData;
252
            }
253
        }
254
        return $results;
255
    }
256
257
    /**
258
     * Gets the cache data for multiple keys.
259
     * 
260
     * @param array  $cacheKeys
261
     * @param string $namespace
262
     * @param string|int $ttl
263
     * @return array
264
     */
265
    public function getMany(array $cacheKeys, string $namespace = '', string|int $ttl = 3600)
266
    {
267
        $ttl = CacheFileHelper::ttl($ttl, $this->defaultTTL);
268
        $results = [];
269
270
        foreach ($cacheKeys as $cacheKey) {
271
            $cacheData = $this->getCache($cacheKey, $namespace, $ttl);
272
            if ($this->isSuccess()) {
273
                $results[$cacheKey] = $cacheData;
274
            } else {
275
                $results[$cacheKey] = null;
276
            }
277
        }
278
279
        return $results;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $results returns the type array which is incompatible with the return type mandated by Silviooosilva\CacheerPhp...eerInterface::getMany() of Silviooosilva\CacheerPhp...face\CacheDataFormatter.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
280
    }
281
282
    /**
283
     * Stores multiple cache items in batches.
284
     * 
285
     * @param array   $items
286
     * @param string  $namespace
287
     * @param integer $batchSize
288
     * @return void
289
     */
290
    public function putMany(array $items, string $namespace = '', int $batchSize = 100)
291
    {
292
        $processedCount = 0;
293
        $itemCount = count($items);
294
295
        while ($processedCount < $itemCount) {
296
            $batchItems = array_slice($items, $processedCount, $batchSize);
297
            $this->processBatchItems($batchItems, $namespace);
298
            $processedCount += count($batchItems);
299
        }
300
    }
301
302
    /**
303
     * Stores an item in the cache with a specific TTL.
304
     * 
305
     * @param string $cacheKey
306
     * @param mixed  $cacheData
307
     * @param string $namespace
308
     * @param string|int $ttl
309
     * @return void
310
     */
311
    public function putCache(string $cacheKey, mixed $cacheData, string $namespace = '', string|int $ttl = 3600)
312
    {
313
        $cacheFile = $this->buildCacheFilePath($cacheKey, $namespace);
314
        $data = $this->fileManager->serialize($cacheData);
315
316
        $this->fileManager->writeFile($cacheFile, $data);
317
        $this->setMessage("Cache file created successfully", true);
318
319
    $this->logger->debug("{$this->getMessage()} from file driver.");
320
}
321
322
    /**
323
     * Checks if a cache key exists.
324
     * 
325
     * @param string $cacheKey
326
     * @param string $namespace
327
     * @return void
328
     */
329
    public function has(string $cacheKey, string $namespace = '')
330
    {
331
        $this->getCache($cacheKey, $namespace);
332
333
        if ($this->isSuccess()) {
334
            $this->setMessage("Cache key: {$cacheKey} exists and it's available! from file driver", true);
335
        } else {
336
            $this->setMessage("Cache key: {$cacheKey} does not exists or it's expired! from file driver", false);
337
        }
338
    }
339
340
    /**
341
     * Renews the cache for a specific key.
342
     * 
343
     * @param string $cacheKey
344
     * @param string|int $ttl
345
     * @param string $namespace
346
     * @return void
347
     */
348
    public function renewCache(string $cacheKey, string|int $ttl, string $namespace = '')
349
    {
350
        $cacheData = $this->getCache($cacheKey, $namespace);
351
        if ($cacheData) {
352
            $this->putCache($cacheKey, $cacheData, $namespace, $ttl);
353
            $this->setMessage("Cache with key {$cacheKey} renewed successfully", true);
354
            $this->logger->debug("{$this->getMessage()} from file driver.");
355
            return;
356
        }
357
        $this->setMessage("Failed to renew Cache with key {$cacheKey}", false);
358
        $this->logger->debug("{$this->getMessage()} from file driver.");
359
    }
360
361
    /**
362
     * Processes a batch of cache items.
363
     * 
364
     * @param array  $batchItems
365
     * @param string $namespace
366
     * @return void
367
     */
368
    private function processBatchItems(array $batchItems, string $namespace)
369
    {
370
        foreach ($batchItems as $item) {
371
            CacheFileHelper::validateCacheItem($item);
372
            $cacheKey = $item['cacheKey'];
373
            $cacheData = $item['cacheData'];
374
            $mergedData = CacheFileHelper::mergeCacheData($cacheData);
375
            $this->putCache($cacheKey, $mergedData, $namespace);
376
        }
377
    }
378
379
    /**
380
     * Checks if the last operation was successful.
381
     * 
382
     * @return boolean
383
     */
384
    public function isSuccess()
385
    {
386
        return $this->success;
387
    }
388
389
    /**
390
     * Sets a message indicating the status of the last operation.
391
     * 
392
     * @param string  $message
393
     * @param boolean $success
394
     * @return void
395
     */
396
    private function setMessage(string $message, bool $success)
397
    {
398
        $this->message = $message;
399
        $this->success = $success;
400
    }
401
402
    /**
403
     * Validates the options provided to the cache store.
404
     * 
405
     * @param array $options
406
     * @return void
407
     */
408
    private function validateOptions(array $options)
409
    {
410
        if (!isset($options['cacheDir']) && $options['drive'] === 'file') {
411
            $this->logger->debug("The 'cacheDir' option is required from file driver.");
412
            throw CacheFileException::create("The 'cacheDir' option is required.");
413
        }
414
        $this->options = $options;
415
    }
416
417
    /**
418
     * Initializes the cache directory.
419
     * 
420
     * @param string $cacheDir
421
     * @return void
422
     */
423
    private function initializeCacheDir(string $cacheDir)
424
    {
425
        $this->cacheDir = realpath($cacheDir) ?: "";
426
        $this->fileManager->createDirectory($cacheDir);
427
    }
428
429
    /**
430
     * Handles the auto-flush functionality based on options.
431
     * 
432
     * @param array $options
433
     * @return void
434
     */
435
    private function handleAutoFlush(array $options)
436
    {
437
        if (isset($options['flushAfter'])) {
438
            $this->scheduleFlush($options['flushAfter']);
439
        }
440
    }
441
442
    /**
443
     * Schedules a flush operation based on the provided interval.
444
     * 
445
     * @param string $flushAfter
446
     * @return void
447
     */
448
    private function scheduleFlush(string $flushAfter)
449
    {
450
        $flushAfterSeconds = CacheFileHelper::convertExpirationToSeconds($flushAfter);
451
452
        if(!$this->fileManager->fileExists($this->lastFlushTimeFile)) {
453
            $this->fileManager->writeFile($this->lastFlushTimeFile, time());
454
            return;
455
        }
456
457
        $lastFlushTime = (int) $this->fileManager->readFile($this->lastFlushTimeFile);
458
459
        if ((time() - $lastFlushTime) >= $flushAfterSeconds) {
460
            $this->flushCache();
461
            $this->fileManager->writeFile($this->lastFlushTimeFile, time());
462
        }
463
    }
464
465
    /**
466
     * Checks if the cache file is valid based on its existence and modification time.
467
     * 
468
     * @param string  $cacheFile
469
     * @param integer $ttl
470
     * @return boolean
471
     */
472
    private function isCacheValid(string $cacheFile, int $ttl)
473
    {
474
        return file_exists($cacheFile) && (filemtime($cacheFile) > (time() - $ttl));
475
    }
476
}