Passed
Push — main ( bce8dd...f4f8ca )
by Sílvio
56s
created

RedisCacheStore   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 404
Duplicated Lines 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 131
dl 0
loc 404
rs 8.8798
c 4
b 1
f 0
wmc 44

18 Methods

Rating   Name   Duplication   Size   Complexity  
A flushCache() 0 9 2
A getDump() 0 3 1
A putMany() 0 9 2
A setMessage() 0 4 1
A __construct() 0 21 4
A appendCache() 0 16 2
A getAll() 0 23 4
A has() 0 13 2
B putCache() 0 25 7
A getMany() 0 18 4
A renewCache() 0 19 3
A processBatchItems() 0 8 2
A clearCache() 0 11 2
A isSuccess() 0 3 1
A getCache() 0 14 2
A buildKey() 0 3 2
A restoreKey() 0 7 2
A getMessage() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like RedisCacheStore 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 RedisCacheStore, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Silviooosilva\CacheerPhp\CacheStore;
4
5
use Exception;
6
use Predis\Response\Status;
7
use Silviooosilva\CacheerPhp\Utils\CacheLogger;
8
use Silviooosilva\CacheerPhp\Helpers\CacheRedisHelper;
9
use Silviooosilva\CacheerPhp\Interface\CacheerInterface;
10
use Silviooosilva\CacheerPhp\Exceptions\CacheRedisException;
11
use Silviooosilva\CacheerPhp\CacheStore\CacheManager\RedisCacheManager;
12
use Silviooosilva\CacheerPhp\CacheStore\CacheManager\GenericFlusher;
13
use Silviooosilva\CacheerPhp\Helpers\CacheFileHelper;
14
use Silviooosilva\CacheerPhp\Helpers\FlushHelper;
15
16
/**
17
 * Class RedisCacheStore
18
 * @author Sílvio Silva <https://github.com/silviooosilva>
19
 * @package Silviooosilva\CacheerPhp
20
 */
21
class RedisCacheStore implements CacheerInterface
22
{
23
    /** @var */
24
    private $redis;
25
26
    /** @param string $namespace */
27
    private string $namespace = '';
28
29
    /**
30
     * @var ?CacheLogger
31
     */
32
    private ?CacheLogger $logger = null;
33
34
    /**
35
     * @var string
36
     */
37
    private string $message = '';
38
39
    /**
40
     * @var boolean
41
     */
42
    private bool $success = false;
43
44
    /** @var int|null */
45
    private ?int $defaultTTL = null;
46
47
    /** @var GenericFlusher|null */
48
    private ?GenericFlusher $flusher = null;
49
50
51
    /**
52
     * RedisCacheStore constructor.
53
     *
54
     * @param string $logPath
55
     * @param array $options
56
     */
57
    public function __construct(string $logPath, array $options = [])
58
    {
59
        $this->redis = RedisCacheManager::connect();
60
        $this->logger = new CacheLogger($logPath);
61
        
62
        // OptionBuilder support
63
        if (!empty($options['namespace'])) {
64
            $this->namespace = (string) $options['namespace'];
65
        }
66
67
        // Default TTL from options
68
        if (!empty($options['expirationTime'])) {
69
            $this->defaultTTL = (int) CacheFileHelper::convertExpirationToSeconds((string) $options['expirationTime']);
70
        }
71
72
        // Auto-flush support
73
        $lastFlushFile = FlushHelper::pathFor('redis', $this->namespace ?: 'default');
74
        $this->flusher = new GenericFlusher($lastFlushFile, function () {
75
            $this->flushCache();
76
        });
77
        $this->flusher->handleAutoFlush($options);
78
    }
79
80
    /**
81
     * Appends data to an existing cache item.
82
     * 
83
     * @param string $cacheKey
84
     * @param mixed  $cacheData
85
     * @param string $namespace
86
     * @return bool
87
     */
88
    public function appendCache(string $cacheKey, mixed $cacheData, string $namespace = ''): void
89
    {
90
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
91
        $existingData = $this->getCache($cacheFullKey);
92
93
        $mergedCacheData = CacheRedisHelper::arrayIdentifier($existingData, $cacheData);
94
95
        $serializedData = CacheRedisHelper::serialize($mergedCacheData);
96
97
        if ($this->redis->set($cacheFullKey, $serializedData)) {
98
            $this->setMessage("Cache appended successfully", true);
99
        } else {
100
            $this->setMessage("Something went wrong. Please, try again.", false);
101
        }
102
103
        $this->logger->debug("{$this->getMessage()} from redis driver.");
0 ignored issues
show
Bug introduced by
The method debug() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

103
        $this->logger->/** @scrutinizer ignore-call */ 
104
                       debug("{$this->getMessage()} from redis driver.");

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
104
    }
105
106
    /**
107
     * Builds a unique key for the Redis cache.
108
     * 
109
     * @param string $key
110
     * @param string $namespace
111
     * @return string
112
     */
113
    private function buildKey(string $key, string $namespace): string
114
    {
115
        return $this->namespace . ($namespace ? $namespace . ':' : '') . $key;
116
    }
117
118
    /**
119
     * Clears a specific cache item.
120
     * 
121
     * @param string $cacheKey
122
     * @param string $namespace
123
     * @return void
124
     */
125
    public function clearCache(string $cacheKey, string $namespace = ''): void
126
    {
127
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
128
129
        if ($this->redis->del($cacheFullKey) > 0) {
130
            $this->setMessage("Cache cleared successfully", true);
131
        } else {
132
            $this->setMessage("Something went wrong. Please, try again.", false);
133
        }
134
135
        $this->logger->debug("{$this->getMessage()} from redis driver.");
136
    }
137
138
    /**
139
     * Flushes all cache items in Redis.
140
     * 
141
     * @return void
142
     */
143
    public function flushCache(): void
144
    {
145
        if ($this->redis->flushall()) {
146
            $this->setMessage("Cache flushed successfully", true);
147
        } else {
148
            $this->setMessage("Something went wrong. Please, try again.", false);
149
        }
150
151
        $this->logger->debug("{$this->getMessage()} from redis driver.");
152
    }
153
154
    /**
155
     * Retrieves a single cache item by its key.
156
     * 
157
     * @param string $cacheKey
158
     * @param string $namespace
159
     * @param string|int $ttl
160
     * @return mixed
161
     */
162
    public function getCache(string $cacheKey, string $namespace = '', string|int $ttl = 3600): mixed
163
    {
164
        $fullCacheKey = $this->buildKey($cacheKey, $namespace);
165
        $cacheData = $this->redis->get($fullCacheKey);
166
167
        if ($cacheData) {
168
            $this->setMessage("Cache retrieved successfully", true);
169
            $this->logger->debug("{$this->getMessage()} from redis driver.");
170
            return CacheRedisHelper::serialize($cacheData, false);
171
        }
172
173
        $this->setMessage("CacheData not found, does not exists or expired", false);
174
        $this->logger->info("{$this->getMessage()} from redis driver.");
175
        return null;
176
    }
177
178
    /**
179
     * Retrieves all cache items in a specific namespace.
180
     * 
181
     * @param string $namespace
182
     * @return array
183
     */
184
    public function getAll(string $namespace = ''): array
185
    {
186
        $keys = $this->redis->keys($this->buildKey('*', $namespace));
187
        $results = [];
188
189
        $prefix = $this->buildKey('', $namespace);
190
        $prefixLen = strlen($prefix);
191
192
        foreach ($keys as $fullKey) {
193
            $cacheKey = substr($fullKey, $prefixLen);
194
            $cacheData = $this->getCache($cacheKey, $namespace);
195
            if ($cacheData !== null) {
196
                $results[$cacheKey] = $cacheData;
197
            }
198
        }
199
200
        if (empty($results)) {
201
            $this->setMessage("No cache data found in the namespace", false);
202
        } else {
203
            $this->setMessage("Cache data retrieved successfully", true);
204
        }
205
206
        return $results;
207
    }
208
209
    /**
210
     * Retrieves multiple cache items by their keys.
211
     * 
212
     * @param array $cacheKeys
213
     * @param string $namespace
214
     * @param string|int $ttl
215
     * @return array
216
     */
217
    public function getMany(array $cacheKeys, string $namespace = '', string|int $ttl = 3600): array
218
    {
219
        $results = [];
220
        foreach ($cacheKeys as $cacheKey) {
221
            $fullCacheKey = $this->buildKey($cacheKey, $namespace);
222
            $cacheData = $this->getCache($fullCacheKey, $namespace, $ttl);
223
            if ($cacheData !== null) {
224
                $results[$cacheKey] = $cacheData;
225
            }
226
        }
227
228
        if (empty($results)) {
229
            $this->setMessage("No cache data found for the provided keys", false);
230
        } else {
231
            $this->setMessage("Cache data retrieved successfully", true);
232
        }
233
234
        return $results;
235
    }
236
237
    /**
238
     * Gets the message from the last operation.
239
     * 
240
     * @return string
241
     */
242
    public function getMessage(): string
243
    {
244
        return $this->message;
245
    }
246
247
    /**
248
     * Gets the serialized dump of a cache item.
249
     * 
250
     * @param string $fullKey
251
     * @return string|null
252
     */
253
    private function getDump(string $fullKey): ?string
254
    {
255
        return $this->redis->dump($fullKey);
256
    }
257
258
    /**
259
     * Checks if a cache item exists.
260
     * 
261
     * @param string $cacheKey
262
     * @param string $namespace
263
     * @return bool
264
     */
265
    public function has(string $cacheKey, string $namespace = ''): bool
266
    {
267
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
268
269
        if ($this->redis->exists($cacheFullKey) > 0) {
270
            $this->setMessage("Cache Key: {$cacheKey} exists!", true);
271
            $this->logger->debug("{$this->getMessage()} from redis driver.");
272
            return true;
273
        }
274
275
        $this->setMessage("Cache Key: {$cacheKey} does not exists!", false);
276
        $this->logger->debug("{$this->getMessage()} from redis driver.");
277
        return false;
278
    }
279
280
    /**
281
     * Checks if the last operation was successful.
282
     * 
283
     * @return boolean
284
     */
285
    public function isSuccess(): bool
286
    {
287
        return $this->success;
288
    }
289
290
    /**
291
     * Processes a batch of cache items and stores them in Redis.
292
     * 
293
     * @param array  $batchItems
294
     * @param string $namespace
295
     * @return void
296
     */
297
    private function processBatchItems(array $batchItems, string $namespace): void
298
    {
299
        foreach ($batchItems as $item) {
300
            CacheRedisHelper::validateCacheItem($item);
301
            $cacheKey = $item['cacheKey'];
302
            $cacheData = $item['cacheData'];
303
            $mergedData = CacheRedisHelper::mergeCacheData($cacheData);
304
            $this->putCache($cacheKey, $mergedData, $namespace);
305
        }
306
    }
307
308
    /**
309
     * Stores a cache item in Redis with optional namespace and TTL.
310
     *
311
     * @param string $cacheKey
312
     * @param mixed  $cacheData
313
     * @param string $namespace
314
     * @param string|int|null $ttl
315
     * @return Status|null
316
     */
317
    public function putCache(string $cacheKey, mixed $cacheData, string $namespace = '', string|int|null $ttl = null): ?Status
318
    {
319
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
320
        $serializedData = CacheRedisHelper::serialize($cacheData);
321
322
        // Resolve TTL with OptionBuilder default when applicable
323
        $ttlToUse = $ttl;
324
        if ($this->defaultTTL !== null && ($ttl === null || (int)$ttl === 3600)) {
325
            $ttlToUse = $this->defaultTTL;
326
        }
327
        if (is_string($ttlToUse)) {
328
            $ttlToUse = (int) CacheFileHelper::convertExpirationToSeconds($ttlToUse);
329
        }
330
331
        $result = $ttlToUse ? $this->redis->setex($cacheFullKey, (int) $ttlToUse, $serializedData)
332
                            : $this->redis->set($cacheFullKey, $serializedData);
333
334
        if ($result) {
335
            $this->setMessage("Cache stored successfully", true);
336
        } else {
337
            $this->setMessage("Failed to store cache", false);
338
        }
339
340
        $this->logger->debug("{$this->getMessage()} from Redis driver.");
341
        return $result;
342
    }
343
344
    /**
345
     * Stores multiple cache items in Redis in batches.
346
     * 
347
     * @param array  $items
348
     * @param string $namespace
349
     * @param int    $batchSize
350
     * @return void
351
     */
352
    public function putMany(array $items, string $namespace = '', int $batchSize = 100): void
353
    {
354
        $processedCount = 0;
355
        $itemCount = count($items);
356
357
        while ($processedCount < $itemCount) {
358
            $batchItems = array_slice($items, $processedCount, $batchSize);
359
            $this->processBatchItems($batchItems, $namespace);
360
            $processedCount += count($batchItems);
361
        }
362
    }
363
364
    /**
365
     * Renews the cache for a specific key with a new TTL.
366
     *
367
     * @param string $cacheKey
368
     * @param string|int $ttl
369
     * @param string $namespace
370
     * @return void
371
     * @throws CacheRedisException
372
     */
373
    public function renewCache(string $cacheKey, string|int $ttl, string $namespace = ''): void
374
    {
375
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
376
        $dump = $this->getDump($cacheFullKey);
377
378
        if (!$dump) {
379
            $this->setMessage("Cache Key: {$cacheKey} not found.", false);
380
            $this->logger->warning("{$this->getMessage()} from Redis driver.");
381
            return;
382
        }
383
384
        $this->clearCache($cacheFullKey);
385
386
        if ($this->restoreKey($cacheFullKey, $ttl, $dump)) {
387
            $this->setMessage("Cache Key: {$cacheKey} renewed successfully.", true);
388
            $this->logger->debug("{$this->getMessage()} from Redis driver.");
389
        } else {
390
            $this->setMessage("Failed to renew cache key: {$cacheKey}.", false);
391
            $this->logger->error("{$this->getMessage()} from Redis driver.");
392
        }
393
    }
394
395
    /**
396
     * Restores a key in Redis with a given TTL and serialized data.
397
     *
398
     * @param string $fullKey
399
     * @param string|int $ttl
400
     * @param mixed $dump
401
     * @return bool
402
     * @throws CacheRedisException
403
     */
404
    private function restoreKey(string $fullKey, string|int $ttl, mixed $dump): bool
405
    {
406
        try {
407
            $this->redis->restore($fullKey, $ttl * 1000, $dump, 'REPLACE');
408
            return true;
409
        } catch (Exception $e) {
410
            throw CacheRedisException::create($e->getMessage());
411
        }
412
    }
413
414
    /**
415
     * Sets a message and its success status.
416
     * 
417
     * @param string  $message
418
     * @param boolean $success
419
     * @return void
420
     */
421
    private function setMessage(string $message, bool $success): void
422
    {
423
        $this->message = $message;
424
        $this->success = $success;
425
    }
426
}
427