Passed
Pull Request — main (#6)
by
unknown
03:29
created

SuperCacheManager::forget()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 32
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 7
eloc 23
c 4
b 0
f 0
nc 10
nop 5
dl 0
loc 32
rs 8.6186
1
<?php
2
3
namespace Padosoft\SuperCache;
4
5
use Padosoft\SuperCache\Traits\ManagesLocksAndShardsTrait;
6
7
class SuperCacheManager
8
{
9
    use ManagesLocksAndShardsTrait;
10
11
    protected RedisConnector $redis;
12
    protected int $numShards;
13
    public string $prefix;
14
    public bool $useNamespace;
15
16
    /**
17
     * Questa proprietà viene settata dinamicamente dove serve in base al nome della connessione
18
     *
19
     * @deprecated
20
     */
21
    public bool $isCluster = false;
22
23
    public function __construct(RedisConnector $redis)
24
    {
25
        $this->redis = $redis;
26
        $this->prefix = config('supercache.prefix');
27
        $this->numShards = (int) config('supercache.num_shards'); // Numero di shard per tag
28
        $this->useNamespace = (bool) config('supercache.use_namespace', false); // Flag per abilitare/disabilitare il namespace
29
    }
30
31
    private function serializeForRedis($value)
32
    {
33
        return is_numeric($value) ? $value : serialize($value);
34
    }
35
36
    private function unserializeForRedis($value)
37
    {
38
        return is_numeric($value) ? $value : unserialize($value);
39
    }
40
41
    /**
42
     * Calcola il namespace in base alla chiave.
43
     */
44
    protected function calculateNamespace(string $key): string
45
    {
46
        // Usa una funzione hash per ottenere un namespace coerente per la chiave
47
        $hash = crc32($key);
48
        $numNamespaces = (int) config('supercache.num_namespace', 16); // Numero di namespace configurabili
49
        $namespaceIndex = $hash % $numNamespaces;
50
51
        return 'ns' . $namespaceIndex; // Ad esempio, 'ns0', 'ns1', ..., 'ns15'
52
    }
53
54
    /**
55
     * Salva un valore nella cache senza tag.
56
     * Il valore della chiave sarà serializzato tranne nel caso di valori numerici
57
     */
58
    public function put(string $key, mixed $value, ?int $ttl = null, ?string $connection_name = null): void
59
    {
60
        // Calcola la chiave con o senza namespace in base alla configurazione
61
        $finalKey = $this->getFinalKey($key);
62
        /**
63
         * $this->redis->getRedisConnection($connection_name)->set($finalKey, $this->serializeForRedis($value));
64
         *
65
         * if ($ttl !== null) {
66
         * $this->redis->getRedisConnection($connection_name)->expire($finalKey, $ttl);
67
         * }
68
         */
69
        if ($ttl !== null) {
70
            $this->redis->getRedisConnection($connection_name)->setEx('{' . $finalKey . '}', $ttl, $this->serializeForRedis($value));
71
72
            return;
73
        }
74
        $this->redis->getRedisConnection($connection_name)->set('{' . $finalKey . '}', $this->serializeForRedis($value));
75
    }
76
77
    public function getTTLKey(string $key, ?string $connection_name = null, bool $isWithTags = false): int
78
    {
79
        // Calcola la chiave con o senza namespace in base alla configurazione
80
        $finalKey = $this->getFinalKey($key, $isWithTags);
81
82
        return $this->redis->getRedisConnection($connection_name)->ttl('{' . $finalKey . '}');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->redis->get...('{' . $finalKey . '}') could return the type true which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
83
    }
84
85
    /**
86
     * Salva un valore nella cache con uno o più tag.
87
     * Il valore della chiave sarà serializzato tranne nel caso di valori numerici
88
     */
89
    public function putWithTags(string $key, mixed $value, array $tags, ?int $ttl = null, ?string $connection_name = null): void
90
    {
91
        $finalKey = $this->getFinalKey($key, true);
92
        $finalTags = $this->getFinalTag($finalKey);
93
        // Usa pipeline solo se non è un cluster
94
        $isCluster = config('database.redis.clusters.' . ($connection_name ?? 'default')) !== null;
95
        if (!$isCluster) {
96
            $this->redis->pipeline(function ($pipe) use ($finalKey, $finalTags, $value, $tags, $ttl) {
97
                // Qui devo mettere le {} perchè così mi assicuro che la chiave e i suoi tags stiano nello stesso has
98
                if ($ttl !== null) {
99
                    $pipe->setEx('{' . $finalKey . '}', $ttl, $this->serializeForRedis($value));
100
                } else {
101
                    $pipe->set('{' . $finalKey . '}', $this->serializeForRedis($value));
102
                }
103
104
                foreach ($tags as $tag) {
105
                    $shard = $this->getShardNameForTag($tag, '{' . $finalKey . '}');
106
                    $pipe->sadd($shard, '{' . $finalKey . '}');
107
                }
108
109
                $pipe->sadd($finalTags, ...$tags);
110
            }, $connection_name);
111
        } else {
112
            if ($ttl !== null) {
113
                $this->redis->getRedisConnection($connection_name)->setEx('{' . $finalKey . '}', $ttl, $this->serializeForRedis($value));
114
            } else {
115
                $result = $this->redis->getRedisConnection($connection_name)->set('{' . $finalKey . '}', $this->serializeForRedis($value));
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
116
            }
117
118
            foreach ($tags as $tag) {
119
                $shard = $this->getShardNameForTag($tag, '{' . $finalKey . '}');
120
                $result = $this->redis->getRedisConnection($connection_name)->sadd($shard, '{' . $finalKey . '}');
121
            }
122
123
            $this->redis->getRedisConnection($connection_name)->sadd($finalTags, ...$tags);
124
        }
125
    }
126
127
    /**
128
     * Memoizza un valore nella cache utilizzando tag specifici.
129
     *
130
     * Questa funzione memorizza un risultato di un callback in cache associato a dei tag specifici.
131
     * Se il valore esiste già nella cache, viene semplicemente restituito. Altrimenti, viene
132
     * eseguito il callback per ottenere il valore, che poi viene memorizzato con i tag specificati.
133
     *
134
     * @param  string      $key             La chiave sotto la quale memorizzare il valore.
135
     * @param  array       $tags            Un array di tag associati al valore memorizzato.
136
     * @param  \Closure    $callback        La funzione di callback che fornisce il valore da memorizzare se non esistente.
137
     * @param  int|null    $ttl             Tempe di vita (time-to-live) in secondi del valore memorizzato. (opzionale)
138
     * @param  string|null $connection_name Il nome della connessione cache da utilizzare. (opzionale)
139
     * @return mixed       Il valore memorizzato e/o recuperato dalla cache.
140
     */
141
    public function rememberWithTags($key, array $tags, \Closure $callback, ?int $ttl = null, ?string $connection_name = null)
142
    {
143
        $finalKey = $this->getFinalKey($key, true);
144
        $value = $this->get($finalKey, $connection_name);
145
146
        // Se esiste già, ok la ritorno
147
        if ($value !== null) {
148
            return $value;
149
        }
150
151
        $value = $callback();
152
153
        $this->putWithTags($finalKey, $value, $tags, $ttl, $connection_name);
154
155
        return $value;
156
    }
157
158
    /**
159
     * Recupera un valore dalla cache.
160
     * Il valore della chiave sarà deserializzato tranne nel caso di valori numerici
161
     */
162
    public function get(string $key, ?string $connection_name = null, bool $isWithTags = false): mixed
163
    {
164
        $finalKey = $this->getFinalKey($key, $isWithTags);
165
166
        $value = $this->redis->getRedisConnection($connection_name)->get('{' . $finalKey . '}');
167
168
        return $value ? $this->unserializeForRedis($value) : null;
169
    }
170
171
    /**
172
     * Rimuove una chiave dalla cache e dai suoi set di tag.
173
     */
174
    public function forget(string $key, ?string $connection_name = null, bool $isFinalKey = false, bool $isWithTags = false, bool $onlyTags = false): void
175
    {
176
        if ($isFinalKey) {
177
            $finalKey = $key;
178
        } else {
179
            $finalKey = $this->getFinalKey($key, $isWithTags);
180
        }
181
        $finalTags = $this->getFinalTag($finalKey);
182
        // Recupera i tag associati alla chiave
183
        $tags = $this->redis->getRedisConnection($connection_name)->smembers($finalTags);
184
        $isCluster = config('database.redis.clusters.' . ($connection_name ?? 'default')) !== null;
185
        if (!$isCluster) {
186
            $this->redis->pipeline(function ($pipe) use ($isWithTags, $onlyTags, $tags, $finalKey, $finalTags) {
0 ignored issues
show
Unused Code introduced by
The import $isWithTags is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
187
                foreach ($tags as $tag) {
188
                    $shard = $this->getShardNameForTag($tag, '{' . $finalKey . '}');
189
                    $pipe->srem($shard, '{' . $finalKey . '}');
190
                }
191
192
                $pipe->del($finalTags);
193
                if (!$onlyTags) {
194
                    $pipe->del('{' . $finalKey . '}');
195
                }
196
            }, $connection_name);
197
        } else {
198
            foreach ($tags as $tag) {
199
                $shard = $this->getShardNameForTag($tag, '{' . $finalKey . '}');
200
                $this->redis->getRedisConnection($connection_name)->srem($shard, '{' . $finalKey . '}');
201
            }
202
203
            $this->redis->getRedisConnection($connection_name)->del($finalTags);
204
            if (!$onlyTags) {
205
                $this->redis->getRedisConnection($connection_name)->del('{' . $finalKey . '}');
206
            }
207
        }
208
    }
209
210
    public function flushByTags(array $tags, ?string $connection_name = null): void
211
    {
212
        // ATTENZIONE, non si può fare in pipeline perchè ci sono anche comandi Redis che hanno bisogno di una promise
213
        // perchè restituiscono dei valori necessari alle istruzioni successive
214
        foreach ($tags as $tag) {
215
            $keys = $this->getKeysOfTag($tag, $connection_name);
216
            foreach ($keys as $key) {
217
                // Con questo cancello sia i tag che le chiavi
218
                $this->forget($key, $connection_name, true, true);
219
            }
220
        }
221
    }
222
223
    /**
224
     * Recupera tutti i tag associati a una chiave.
225
     */
226
    public function getTagsOfKey(string $key, ?string $connection_name = null): array
227
    {
228
        $finalKey = $this->getFinalKey($key, true);
229
        $finalTags = $this->getFinalTag($finalKey);
230
231
        return $this->redis->getRedisConnection($connection_name)->smembers($finalTags);
232
    }
233
234
    /**
235
     * Recupera tutte le chiavi associate a un tag.
236
     */
237
    public function getKeysOfTag(string $tag, ?string $connection_name = null, bool $isfinalTag = false): array
238
    {
239
        if ($isfinalTag) {
240
            return $this->redis->getRedisConnection($connection_name)->smembers($tag);
241
        }
242
        $keys = [];
243
244
        // Itera attraverso tutti gli shard del tag
245
        for ($i = 0; $i < $this->numShards; $i++) {
246
            $shard = $this->prefix . 'tag:' . $tag . ':shard:' . $i;
247
            $keys = array_merge($keys, $this->redis->getRedisConnection($connection_name)->smembers($shard));
248
        }
249
250
        return $keys;
251
    }
252
253
    /**
254
     * Ritorna il nome dello shard per una chiave e un tag.
255
     */
256
    public function getShardNameForTag(string $tag, string $key): string
257
    {
258
        // Usa la funzione hash per calcolare lo shard della chiave
259
        $hash = crc32($key);
260
        $shardIndex = $hash % $this->numShards;
261
262
        return $this->prefix . 'tag:' . $tag . ':shard:' . $shardIndex;
263
    }
264
265
    /**
266
     * Aggiunge il namespace come suffisso alla chiave se abilitato.
267
     *
268
     * Se l'opzione 'use_namespace' è disattivata, la chiave sarà formata senza namespace.
269
     */
270
    public function getFinalKey(string $key, bool $isWithTags = false): string
271
    {
272
        // Se il namespace è abilitato, calcola la chiave con namespace come suffisso
273
        if ($this->useNamespace) {
274
            $namespace = $this->calculateNamespace($key);
275
276
            return $this->prefix . $key . ':' . ($isWithTags ? 'byTags:' : '') . $namespace;
277
        }
278
279
        // Se il namespace è disabilitato, usa la chiave senza suffisso
280
        return $this->prefix . $key . ($isWithTags ? ':byTags' : '');
281
    }
282
283
    public function getFinalTag(string $finalKey): string
284
    {
285
        return '{' . $finalKey . '}:tags';
286
    }
287
288
    /**
289
     * Flush all cache entries.
290
     */
291
    public function flush(?string $connection_name = null): void
292
    {
293
        $this->redis->getRedisConnection($connection_name)->flushall(); // Svuota tutto il database Redis
294
    }
295
296
    /**
297
     * Check if a cache key exists without retrieving the value.
298
     */
299
    public function has(string $key, ?string $connection_name = null, bool $isWithTags = false, bool $isfinalKey = false): bool
300
    {
301
        if ($isfinalKey) {
302
            $finalKey = $key;
303
        } else {
304
            $finalKey = '{' . $this->getFinalKey($key, $isWithTags) . '}';
305
        }
306
307
        return $this->redis->getRedisConnection($connection_name)->exists($finalKey) > 0;
308
    }
309
310
    /**
311
     * Increment a cache key by a given amount.
312
     * If the key does not exist, creates it with the increment value.
313
     *
314
     * @return int The new value after incrementing.
315
     */
316
    public function increment(string $key, int $increment = 1, ?string $connection_name = null): int
317
    {
318
        $finalKey = $this->getFinalKey($key);
319
320
        return $this->redis->getRedisConnection($connection_name)->incrby($finalKey, $increment);
321
    }
322
323
    /**
324
     * Decrement a cache key by a given amount.
325
     * If the key does not exist, creates it with the negative decrement value.
326
     *
327
     * @return int The new value after decrementing.
328
     */
329
    public function decrement(string $key, int $decrement = 1, ?string $connection_name = null): int
330
    {
331
        $finalKey = $this->getFinalKey($key);
332
333
        return $this->redis->getRedisConnection($connection_name)->decrby($finalKey, $decrement);
334
    }
335
336
    /**
337
     * Get all keys matching given patterns.
338
     *
339
     * @param  array $patterns An array of patterns (e.g. ["product:*"])
340
     * @return array Array of key-value pairs.
341
     */
342
    public function getKeys(array $patterns, ?string $connection_name = null): array
343
    {
344
        $results = [];
345
        foreach ($patterns as $pattern) {
346
            // Trova le chiavi che corrispondono al pattern usando SCAN
347
            $iterator = null;
348
            // Keys terminato il loop ritorna un false
349
            $tempArrKeys = [];
350
            while ($keys = $this->redis->getRedisConnection($connection_name)->scan(
351
                $iterator,
352
                [
353
                    'match' => $pattern,
354
                    'count' => 20,
355
                ]
356
            )) {
357
                $iterator = $keys[0];
358
359
                foreach ($keys[1] as $key) {
360
                    if ($key === null) {
361
                        continue;
362
                    }
363
                    $tempArrKeys[] = $key;
364
365
                    $original_key = $this->getOriginalKey($key);
366
                    $value = $this->get($original_key);
367
                    $results[$original_key] = $value;
368
                }
369
            }
370
        }
371
372
        return $results;
373
    }
374
375
    public function getOriginalKey(string $finalKey): string
376
    {
377
        $originalKey = str_replace([config('database.redis.options')['prefix'], $this->prefix, '{', '}'], '', $finalKey);
378
        if (!$this->useNamespace) {
379
            return $originalKey;
380
        }
381
        $pattern = '/:ns\d+/';
382
383
        return preg_replace($pattern, '', $originalKey);
384
    }
385
386
    /**
387
     * Acquire a lock.
388
     *
389
     * @param  string $key The lock key.
390
     * @return bool   True if the lock was acquired, false otherwise.
391
     */
392
    public function lock(string $key, ?string $connection_name = null, int $ttl = 10, string $value = '1'): bool
393
    {
394
        $finalKey = $this->getFinalKey($key) . ':semaphore';
395
        $luaScript = <<<'LUA'
396
        if redis.call("SET", KEYS[1], ARGV[2], "NX", "EX", tonumber(ARGV[1])) then
397
            return 1
398
        else
399
            return 0
400
        end
401
        LUA;
402
403
        $result = $this->redis->getRedisConnection($connection_name)->eval(
404
            $luaScript,
405
            1, // Number of keys
406
            $finalKey,
407
            $ttl,
408
            $value
409
        );
410
411
        return $result === 1;
412
    }
413
414
    /**
415
     * Rilascia un lock precedentemente acquisito.
416
     *
417
     * @param string      $key             La chiave del lock da rilasciare.
418
     * @param string|null $connection_name Il nome della connessione opzionale da utilizzare. Se null, verrà utilizzata la connessione predefinita.
419
     */
420
    public function unLock(string $key, ?string $connection_name = null): void
421
    {
422
        $finalKey = $this->getFinalKey($key) . ':semaphore';
423
        $luaScript = <<<'LUA'
424
        redis.call('DEL', KEYS[1]);
425
        LUA;
426
        $this->redis->getRedisConnection($connection_name)->eval(
427
            $luaScript,
428
            1, // Number of keys
429
            $finalKey
430
        );
431
    }
432
}
433