Passed
Branch main (457442)
by Sílvio
03:14
created

FileCacheStore   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 425
Duplicated Lines 0 %

Importance

Changes 7
Bugs 2 Features 1
Metric Value
eloc 123
c 7
b 2
f 1
dl 0
loc 425
rs 8.8798
wmc 44

22 Methods

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