Passed
Branch main (8837c4)
by Sílvio
14:13
created

RedisCacheStore::getMany()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
c 0
b 0
f 0
nc 6
nop 3
dl 0
loc 17
rs 9.9332
1
<?php
2
3
namespace Silviooosilva\CacheerPhp\CacheStore;
4
5
use Exception;
6
use Predis\Response\Status;
7
use Silviooosilva\CacheerPhp\Helpers\CacheRedisHelper;
8
use Silviooosilva\CacheerPhp\Interface\CacheerInterface;
9
use Silviooosilva\CacheerPhp\Interface\CacheReadStoreInterface;
10
use Silviooosilva\CacheerPhp\Interface\CacheWriteStoreInterface;
11
use Silviooosilva\CacheerPhp\Interface\TaggableCacheStoreInterface;
12
use Silviooosilva\CacheerPhp\Exceptions\CacheRedisException;
13
use Silviooosilva\CacheerPhp\CacheStore\CacheManager\RedisCacheManager;
14
use Silviooosilva\CacheerPhp\CacheStore\CacheManager\GenericFlusher;
15
use Silviooosilva\CacheerPhp\Helpers\CacheFileHelper;
16
use Silviooosilva\CacheerPhp\Helpers\FlushHelper;
17
use Silviooosilva\CacheerPhp\Enums\CacheStoreType;
18
use Silviooosilva\CacheerPhp\CacheStore\Support\OperationStatus;
19
use Silviooosilva\CacheerPhp\CacheStore\Support\RedisBatchWriter;
20
use Silviooosilva\CacheerPhp\CacheStore\Support\RedisKeyspace;
21
use Silviooosilva\CacheerPhp\CacheStore\Support\RedisTagIndex;
22
use Silviooosilva\CacheerPhp\Utils\CacheLogger;
23
24
/**
25
 * Class RedisCacheStore
26
 * @author Sílvio Silva <https://github.com/silviooosilva>
27
 * @package Silviooosilva\CacheerPhp
28
 */
29
class RedisCacheStore implements CacheerInterface, CacheReadStoreInterface, CacheWriteStoreInterface, TaggableCacheStoreInterface
30
{
31
    /** @var */
32
    private $redis;
33
34
    /**
35
     * Handles namespace/key formatting and TTL decisions.
36
     */
37
    private RedisKeyspace $keyspace;
38
39
    /**
40
     * Tracks state and logging.
41
     */
42
    private OperationStatus $status;
43
44
    /** @var GenericFlusher|null */
45
    private ?GenericFlusher $flusher = null;
46
47
    /**
48
     * Manages tag membership.
49
     */
50
    private RedisTagIndex $tagIndex;
51
52
53
    /**
54
     * RedisCacheStore constructor.
55
     *
56
     * @param string $logPath
57
     * @param array $options
58
     */
59
    public function __construct(string $logPath, array $options = [])
60
    {
61
        $this->redis = RedisCacheManager::connect();
62
        $logger = new CacheLogger($logPath);
63
64
        $namespace = !empty($options['namespace']) ? (string) $options['namespace'] : '';
65
        $defaultTTL = null;
66
67
        if (!empty($options['expirationTime'])) {
68
            $defaultTTL = (int) CacheFileHelper::convertExpirationToSeconds((string) $options['expirationTime']);
69
        }
70
71
        $this->keyspace = new RedisKeyspace($namespace, $defaultTTL);
72
        $this->status = new OperationStatus($logger);
73
        $this->tagIndex = new RedisTagIndex($this->redis, $this->keyspace, $this->status);
74
75
        // Auto-flush support
76
        $lastFlushFile = FlushHelper::pathFor(CacheStoreType::REDIS, $namespace ?: 'default');
77
        $this->flusher = new GenericFlusher($lastFlushFile, function () {
78
            $this->flushCache();
79
        });
80
        $this->flusher->handleAutoFlush($options);
81
    }
82
83
    /**
84
     * Appends data to an existing cache item.
85
     * 
86
     * @param string $cacheKey
87
     * @param mixed  $cacheData
88
     * @param string $namespace
89
     * @return bool
90
     */
91
    public function appendCache(string $cacheKey, mixed $cacheData, string $namespace = ''): void
92
    {
93
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
94
        $existingData = $this->getCache($cacheKey, $namespace);
95
96
        $mergedCacheData = CacheRedisHelper::arrayIdentifier($existingData, $cacheData);
97
98
        $serializedData = CacheRedisHelper::serialize($mergedCacheData);
99
100
        if ($this->redis->set($cacheFullKey, $serializedData)) {
101
            $this->status->record("Cache appended successfully", true);
102
        } else {
103
            $this->status->record("Something went wrong. Please, try again.", false);
104
        }
105
    }
106
107
    /**
108
     * Builds a unique key for the Redis cache.
109
     * 
110
     * @param string $key
111
     * @param string $namespace
112
     * @return string
113
     */
114
    private function buildKey(string $key, string $namespace): string
115
    {
116
        return $this->keyspace->build($key, $namespace);
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 = ''): void
127
    {
128
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
129
130
        if ($this->redis->del($cacheFullKey) > 0) {
131
            $this->status->record("Cache cleared successfully", true);
132
        } else {
133
            $this->status->record("Something went wrong. Please, try again.", false);
134
        }
135
    }
136
137
    /**
138
     * Flushes all cache items in Redis.
139
     * 
140
     * @return void
141
     */
142
    public function flushCache(): void
143
    {
144
        if ($this->redis->flushall()) {
145
            $this->status->record("Cache flushed successfully", true);
146
        } else {
147
            $this->status->record("Something went wrong. Please, try again.", false);
148
        }
149
    }
150
151
    /**
152
     * Associates one or more keys to a tag using a Redis Set.
153
     *
154
     * @param string $tag
155
     * @param string ...$keys
156
     * @return bool
157
     */
158
    public function tag(string $tag, string ...$keys): bool
159
    {
160
        return $this->tagIndex->tag($tag, ...$keys);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->tagIndex->tag($tag, $keys) returns the type boolean which is incompatible with the return type mandated by Silviooosilva\CacheerPhp...heStoreInterface::tag() of void.

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...
161
    }
162
163
    /**
164
     * Flush all keys associated with a tag.
165
     *
166
     * @param string $tag
167
     * @return void
168
     */
169
    public function flushTag(string $tag): void
170
    {
171
        $this->tagIndex->flush($tag, function (string $cacheKey, string $namespace): void {
172
            $this->clearCache($cacheKey, $namespace);
173
        });
174
    }
175
176
    /**
177
     * Retrieves a single cache item by its key.
178
     * 
179
     * @param string $cacheKey
180
     * @param string $namespace
181
     * @param string|int $ttl
182
     * @return mixed
183
     */
184
    public function getCache(string $cacheKey, string $namespace = '', string|int $ttl = 3600): mixed
185
    {
186
        $fullCacheKey = $this->buildKey($cacheKey, $namespace);
187
        $cacheData = $this->redis->get($fullCacheKey);
188
189
        if ($cacheData) {
190
            $this->status->record("Cache retrieved successfully", true);
191
            return CacheRedisHelper::serialize($cacheData, false);
192
        }
193
194
        $this->status->record("CacheData not found, does not exists or expired", false, 'info');
195
        return null;
196
    }
197
198
    /**
199
     * Retrieves all cache items in a specific namespace.
200
     * 
201
     * @param string $namespace
202
     * @return array
203
     */
204
    public function getAll(string $namespace = ''): array
205
    {
206
        $keys = $this->redis->keys($this->buildKey('*', $namespace));
207
        $results = [];
208
209
        $prefix = $this->buildKey('', $namespace);
210
        $prefixLen = strlen($prefix);
211
212
        foreach ($keys as $fullKey) {
213
            $cacheKey = substr($fullKey, $prefixLen);
214
            $cacheData = $this->getCache($cacheKey, $namespace);
215
            if ($cacheData !== null) {
216
                $results[$cacheKey] = $cacheData;
217
            }
218
        }
219
220
        if (empty($results)) {
221
            $this->status->record("No cache data found in the namespace", false);
222
        } else {
223
            $this->status->record("Cache data retrieved successfully", true);
224
        }
225
226
        return $results;
227
    }
228
229
    /**
230
     * Retrieves multiple cache items by their keys.
231
     * 
232
     * @param array $cacheKeys
233
     * @param string $namespace
234
     * @param string|int $ttl
235
     * @return array
236
     */
237
    public function getMany(array $cacheKeys, string $namespace = '', string|int $ttl = 3600): array
238
    {
239
        $results = [];
240
        foreach ($cacheKeys as $cacheKey) {
241
            $cacheData = $this->getCache($cacheKey, $namespace, $ttl);
242
            if ($cacheData !== null) {
243
                $results[$cacheKey] = $cacheData;
244
            }
245
        }
246
247
        if (empty($results)) {
248
            $this->status->record("No cache data found for the provided keys", false);
249
        } else {
250
            $this->status->record("Cache data retrieved successfully", true);
251
        }
252
253
        return $results;
254
    }
255
256
    /**
257
     * Gets the message from the last operation.
258
     * 
259
     * @return string
260
     */
261
    public function getMessage(): string
262
    {
263
        return $this->status->getMessage();
264
    }
265
266
    /**
267
     * Gets the serialized dump of a cache item.
268
     * 
269
     * @param string $fullKey
270
     * @return string|null
271
     */
272
    private function getDump(string $fullKey): ?string
273
    {
274
        return $this->redis->dump($fullKey);
275
    }
276
277
    /**
278
     * Checks if a cache item exists.
279
     * 
280
     * @param string $cacheKey
281
     * @param string $namespace
282
     * @return bool
283
     */
284
    public function has(string $cacheKey, string $namespace = ''): bool
285
    {
286
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
287
288
        if ($this->redis->exists($cacheFullKey) > 0) {
289
            $this->status->record("Cache Key: {$cacheKey} exists!", true);
290
            return true;
291
        }
292
293
        $this->status->record("Cache Key: {$cacheKey} does not exists!", false);
294
        return false;
295
    }
296
297
    /**
298
     * Checks if the last operation was successful.
299
     * 
300
     * @return boolean
301
     */
302
    public function isSuccess(): bool
303
    {
304
        return $this->status->isSuccess();
305
    }
306
307
    /**
308
     * Stores a cache item in Redis with optional namespace and TTL.
309
     *
310
     * @param string $cacheKey
311
     * @param mixed  $cacheData
312
     * @param string $namespace
313
     * @param string|int|null $ttl
314
     * @return Status|null
315
     */
316
    public function putCache(string $cacheKey, mixed $cacheData, string $namespace = '', string|int $ttl = 3600): ?Status
317
    {
318
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
319
        $serializedData = CacheRedisHelper::serialize($cacheData);
320
321
        $ttlToUse = $this->keyspace->resolveTTL($ttl);
322
323
        $result = $ttlToUse ? $this->redis->setex($cacheFullKey, (int) $ttlToUse, $serializedData)
324
                            : $this->redis->set($cacheFullKey, $serializedData);
325
326
        if ($result) {
327
            $this->status->record("Cache stored successfully", true);
328
        } else {
329
            $this->status->record("Failed to store cache", false);
330
        }
331
332
        return $result;
333
    }
334
335
    /**
336
     * Stores multiple cache items in Redis in batches.
337
     * 
338
     * @param array  $items
339
     * @param string $namespace
340
     * @param int    $batchSize
341
     * @return void
342
     */
343
    public function putMany(array $items, string $namespace = '', int $batchSize = 100): void
344
    {
345
        $writer = new RedisBatchWriter($batchSize);
346
        $writer->write($items, $namespace, function (string $cacheKey, mixed $cacheData, string $ns): void {
347
            $this->putCache($cacheKey, $cacheData, $ns);
348
        });
349
    }
350
351
    /**
352
     * Renews the cache for a specific key with a new TTL.
353
     *
354
     * @param string $cacheKey
355
     * @param string|int $ttl
356
     * @param string $namespace
357
     * @return void
358
     * @throws CacheRedisException
359
     */
360
    public function renewCache(string $cacheKey, string|int $ttl, string $namespace = ''): void
361
    {
362
        $cacheFullKey = $this->buildKey($cacheKey, $namespace);
363
        $dump = $this->getDump($cacheFullKey);
364
365
        if (!$dump) {
366
            $this->status->record("Cache Key: {$cacheKey} not found.", false, 'warning');
367
            return;
368
        }
369
370
        $this->clearCache($cacheKey, $namespace);
371
372
        if ($this->restoreKey($cacheFullKey, $ttl, $dump)) {
373
            $this->status->record("Cache Key: {$cacheKey} renewed successfully.", true);
374
        } else {
375
            $this->status->record("Failed to renew cache key: {$cacheKey}.", false, 'error');
376
        }
377
    }
378
379
    /**
380
     * Restores a key in Redis with a given TTL and serialized data.
381
     *
382
     * @param string $fullKey
383
     * @param string|int $ttl
384
     * @param mixed $dump
385
     * @return bool
386
     * @throws CacheRedisException
387
     */
388
    private function restoreKey(string $fullKey, string|int $ttl, mixed $dump): bool
389
    {
390
        try {
391
            $this->redis->restore($fullKey, $ttl * 1000, $dump, 'REPLACE');
392
            return true;
393
        } catch (Exception $e) {
394
            throw CacheRedisException::create($e->getMessage());
395
        }
396
    }
397
398
}
399