Passed
Push — main ( acdca4...27a10c )
by
unknown
03:27
created

SuperCacheManager::forget()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
cc 6
eloc 24
c 8
b 0
f 0
nc 8
nop 5
dl 0
loc 36
rs 8.9137
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
        if ($ttl !== null) {
63
            $this->redis->getRedisConnection($connection_name)->setEx($finalKey, $ttl, $this->serializeForRedis($value));
64
65
            return;
66
        }
67
        $this->redis->getRedisConnection($connection_name)->set($finalKey, $this->serializeForRedis($value));
68
    }
69
70
    public function getTTLKey(string $key, ?string $connection_name = null, bool $isWithTags = false): int
71
    {
72
        // Calcola la chiave con o senza namespace in base alla configurazione
73
        $finalKey = $this->getFinalKey($key, $isWithTags);
74
75
        return $this->redis->getRedisConnection($connection_name)->ttl($finalKey);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->redis->get...n_name)->ttl($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...
76
    }
77
78
    /**
79
     * Salva un valore nella cache con uno o più tag.
80
     * Il valore della chiave sarà serializzato tranne nel caso di valori numerici
81
     */
82
    public function putWithTags(string $key, mixed $value, array $tags, ?int $ttl = null, ?string $connection_name = null): void
83
    {
84
        $finalKey = $this->getFinalKey($key, true);
85
        // Usa pipeline solo se non è un cluster
86
        $isCluster = config('database.redis.clusters.' . ($connection_name ?? 'default')) !== null;
87
        $advancedMode = config('supercache.advancedMode', 0) === 1;
88
        if (!$isCluster) {
89
            $this->redis->pipeline(function ($pipe) use ($finalKey, $value, $tags, $ttl, $advancedMode) {
90
                if ($ttl !== null) {
91
                    $pipe->setEx($finalKey, $ttl, $this->serializeForRedis($value));
92
                } else {
93
                    $pipe->set($finalKey, $this->serializeForRedis($value));
94
                }
95
96
                foreach ($tags as $tag) {
97
                    $shard = $this->getShardNameForTag($tag, $finalKey);
98
                    $pipe->sadd($shard, $finalKey);
99
                }
100
101
                if ($advancedMode) {
102
                    $pipe->sadd($this->prefix . 'tags:' . $finalKey, ...$tags);
103
                }
104
            }, $connection_name);
105
        } else {
106
            if ($ttl !== null) {
107
                $this->redis->getRedisConnection($connection_name)->setEx($finalKey, $ttl, $this->serializeForRedis($value));
108
            } else {
109
                $this->redis->getRedisConnection($connection_name)->set($finalKey, $this->serializeForRedis($value));
110
            }
111
            foreach ($tags as $tag) {
112
                $shard = $this->getShardNameForTag($tag, $finalKey);
113
                $this->redis->getRedisConnection($connection_name)->sadd($shard, $finalKey);
114
            }
115
            if ($advancedMode) {
116
                $this->redis->getRedisConnection($connection_name)->sadd($this->prefix . 'tags:' . $finalKey, ...$tags);
117
            }
118
        }
119
    }
120
121
    /**
122
     * Memoizza un valore nella cache utilizzando tag specifici.
123
     *
124
     * Questa funzione memorizza un risultato di un callback in cache associato a dei tag specifici.
125
     * Se il valore esiste già nella cache, viene semplicemente restituito. Altrimenti, viene
126
     * eseguito il callback per ottenere il valore, che poi viene memorizzato con i tag specificati.
127
     *
128
     * @param  string      $key             La chiave sotto la quale memorizzare il valore.
129
     * @param  array       $tags            Un array di tag associati al valore memorizzato.
130
     * @param  \Closure    $callback        La funzione di callback che fornisce il valore da memorizzare se non esistente.
131
     * @param  int|null    $ttl             Tempe di vita (time-to-live) in secondi del valore memorizzato. (opzionale)
132
     * @param  string|null $connection_name Il nome della connessione cache da utilizzare. (opzionale)
133
     * @return mixed       Il valore memorizzato e/o recuperato dalla cache.
134
     */
135
    public function rememberWithTags($key, array $tags, \Closure $callback, ?int $ttl = null, ?string $connection_name = null)
136
    {
137
        $finalKey = $this->getFinalKey($key, true);
138
        $value = $this->get($finalKey, $connection_name);
139
140
        // Se esiste già, ok la ritorno
141
        if ($value !== null) {
142
            return $value;
143
        }
144
145
        $value = $callback();
146
147
        $this->putWithTags($key, $value, $tags, $ttl, $connection_name);
148
149
        return $value;
150
    }
151
152
    /**
153
     * Recupera un valore dalla cache.
154
     * Il valore della chiave sarà deserializzato tranne nel caso di valori numerici
155
     */
156
    public function get(string $key, ?string $connection_name = null, bool $isWithTags = false): mixed
157
    {
158
        $finalKey = $this->getFinalKey($key, $isWithTags);
159
160
        $value = $this->redis->getRedisConnection($connection_name)->get($finalKey);
161
162
        return $value ? $this->unserializeForRedis($value) : null;
163
    }
164
165
    /**
166
     * Rimuove una chiave dalla cache e dai suoi set di tag.
167
     */
168
    public function forget(string $key, ?string $connection_name = null, bool $isFinalKey = false, bool $isWithTags = false, bool $onlyTags = false): void
169
    {
170
        if ($isFinalKey) {
171
            $finalKey = $key;
172
        } else {
173
            $finalKey = $this->getFinalKey($key, $isWithTags);
174
        }
175
        $advancedMode = config('supercache.advancedMode', 0) === 1;
176
177
        if (!$advancedMode) {
178
            $this->redis->getRedisConnection($connection_name)->del($finalKey);
179
180
            return;
181
        }
182
183
        // Recupera i tag associati alla chiave
184
        $tags = $this->redis->getRedisConnection($connection_name)->smembers($this->prefix . 'tags:' . $finalKey);
185
        $isCluster = config('database.redis.clusters.' . ($connection_name ?? 'default')) !== null;
186
        if (!$isCluster) {
187
            $this->redis->pipeline(function ($pipe) use ($tags, $finalKey) {
188
                foreach ($tags as $tag) {
189
                    $shard = $this->getShardNameForTag($tag, $finalKey);
190
                    $pipe->srem($shard, $finalKey);
191
                }
192
193
                $pipe->del($this->prefix . 'tags:' . $finalKey);
194
                $pipe->del($finalKey);
195
            }, $connection_name);
196
        } else {
197
            foreach ($tags as $tag) {
198
                $shard = $this->getShardNameForTag($tag, $finalKey);
199
                $this->redis->getRedisConnection($connection_name)->srem($shard, $finalKey);
200
            }
201
202
            $this->redis->getRedisConnection($connection_name)->del($this->prefix . 'tags:' . $finalKey);
203
            $this->redis->getRedisConnection($connection_name)->del($finalKey);
204
        }
205
    }
206
207
    public function flushByTags(array $tags, ?string $connection_name = null): void
208
    {
209
        // ATTENZIONE, non si può fare in pipeline perchè ci sono anche comandi Redis che hanno bisogno di una promise
210
        // perchè restituiscono dei valori necessari alle istruzioni successive
211
        $advancedMode = config('supercache.advancedMode', 0) === 1;
212
        foreach ($tags as $tag) {
213
            $keys = $this->getKeysOfTag($tag, $connection_name);
214
            foreach ($keys as $key) {
215
                // Con questo cancello sia i tag che le chiavi
216
                $this->forget($key, $connection_name, true, true);
217
                if (!$advancedMode) {
218
                    $shard = $this->getShardNameForTag($tag, $key);
219
                    $this->redis->getRedisConnection($connection_name)->srem($shard, $key);
220
                }
221
            }
222
        }
223
    }
224
225
    /**
226
     * Recupera tutti i tag associati a una chiave.
227
     */
228
    public function getTagsOfKey(string $key, ?string $connection_name = null): array
229
    {
230
        $finalKey = $this->getFinalKey($key, true);
231
232
        return $this->redis->getRedisConnection($connection_name)->smembers($this->prefix . 'tags:' . $finalKey);
233
    }
234
235
    /**
236
     * Recupera tutte le chiavi associate a un tag.
237
     */
238
    public function getKeysOfTag(string $tag, ?string $connection_name = null, bool $isfinalTag = false): array
239
    {
240
        if ($isfinalTag) {
241
            return $this->redis->getRedisConnection($connection_name)->smembers($tag);
242
        }
243
        $keys = [];
244
245
        // Itera attraverso tutti gli shard del tag
246
        for ($i = 0; $i < $this->numShards; $i++) {
247
            $shard = $this->prefix . 'tag:' . $tag . ':shard:' . $i;
248
            $keys = array_merge($keys, $this->redis->getRedisConnection($connection_name)->smembers($shard));
249
        }
250
251
        return $keys;
252
    }
253
254
    /**
255
     * Ritorna il nome dello shard per una chiave e un tag.
256
     */
257
    public function getShardNameForTag(string $tag, string $key): string
258
    {
259
        // Usa la funzione hash per calcolare lo shard della chiave
260
        $hash = crc32($key);
261
        $shardIndex = $hash % $this->numShards;
262
263
        return $this->prefix . 'tag:' . $tag . ':shard:' . $shardIndex;
264
    }
265
266
    /**
267
     * Aggiunge il namespace come suffisso alla chiave se abilitato.
268
     *
269
     * Se l'opzione 'use_namespace' è disattivata, la chiave sarà formata senza namespace.
270
     */
271
    public function getFinalKey(string $key, bool $isWithTags = false): string
272
    {
273
        // Se il namespace è abilitato, calcola la chiave con namespace come suffisso
274
        if ($this->useNamespace) {
275
            $namespace = $this->calculateNamespace($key);
276
277
            return $this->prefix . $key . ':' . $namespace;
278
        }
279
280
        // Se il namespace è disabilitato, usa la chiave senza suffisso
281
        return $this->prefix . $key;
282
    }
283
284
    /**
285
     * Flush all cache entries.
286
     */
287
    public function flush(?string $connection_name = null): void
288
    {
289
        $this->redis->getRedisConnection($connection_name)->flushall(); // Svuota tutto il database Redis
290
    }
291
292
    /**
293
     * Check if a cache key exists without retrieving the value.
294
     */
295
    public function has(string $key, ?string $connection_name = null, bool $isWithTags = false, bool $isfinalKey = false): bool
296
    {
297
        if ($isfinalKey) {
298
            $finalKey = $key;
299
        } else {
300
            $finalKey = $this->getFinalKey($key, $isWithTags);
301
        }
302
303
        return $this->redis->getRedisConnection($connection_name)->exists($finalKey) > 0;
304
    }
305
306
    /**
307
     * Increment a cache key by a given amount.
308
     * If the key does not exist, creates it with the increment value.
309
     *
310
     * @return int The new value after incrementing.
311
     */
312
    public function increment(string $key, int $increment = 1, ?string $connection_name = null): int
313
    {
314
        $finalKey = $this->getFinalKey($key);
315
316
        return $this->redis->getRedisConnection($connection_name)->incrby($finalKey, $increment);
317
    }
318
319
    /**
320
     * Decrement a cache key by a given amount.
321
     * If the key does not exist, creates it with the negative decrement value.
322
     *
323
     * @return int The new value after decrementing.
324
     */
325
    public function decrement(string $key, int $decrement = 1, ?string $connection_name = null): int
326
    {
327
        $finalKey = $this->getFinalKey($key);
328
329
        return $this->redis->getRedisConnection($connection_name)->decrby($finalKey, $decrement);
330
    }
331
332
    /**
333
     * Get all keys matching given patterns.
334
     *
335
     * @param  array $patterns An array of patterns (e.g. ["product:*"])
336
     * @return array Array of key-value pairs.
337
     */
338
    public function getKeys(array $patterns, ?string $connection_name = null): array
339
    {
340
        $results = [];
341
        foreach ($patterns as $pattern) {
342
            // Trova le chiavi che corrispondono al pattern usando SCAN
343
            $iterator = null;
344
            // Keys terminato il loop ritorna un false
345
            $tempArrKeys = [];
346
            while ($keys = $this->redis->getRedisConnection($connection_name)->scan(
347
                $iterator,
348
                [
349
                    'match' => $pattern,
350
                    'count' => 20,
351
                ]
352
            )) {
353
                $iterator = $keys[0];
354
355
                foreach ($keys[1] as $key) {
356
                    if ($key === null) {
357
                        continue;
358
                    }
359
                    $tempArrKeys[] = $key;
360
361
                    $original_key = $this->getOriginalKey($key);
362
                    $value = $this->get($original_key);
363
                    $results[$original_key] = $value;
364
                }
365
            }
366
        }
367
368
        return $results;
369
    }
370
371
    public function getOriginalKey(string $finalKey): string
372
    {
373
        $originalKey = str_replace([config('database.redis.options')['prefix'], $this->prefix], '', $finalKey);
374
        if (!$this->useNamespace) {
375
            return $originalKey;
376
        }
377
        $pattern = '/:ns\d+/';
378
379
        return preg_replace($pattern, '', $originalKey);
380
    }
381
382
    /**
383
     * Acquire a lock.
384
     *
385
     * @param  string $key The lock key.
386
     * @return bool   True if the lock was acquired, false otherwise.
387
     */
388
    public function lock(string $key, ?string $connection_name = null, int $ttl = 10, string $value = '1'): bool
389
    {
390
        $finalKey = $this->getFinalKey($key) . ':semaphore';
391
        $luaScript = <<<'LUA'
392
        if redis.call("SET", KEYS[1], ARGV[2], "NX", "EX", tonumber(ARGV[1])) then
393
            return 1
394
        else
395
            return 0
396
        end
397
        LUA;
398
399
        $result = $this->redis->getRedisConnection($connection_name)->eval(
400
            $luaScript,
401
            1, // Number of keys
402
            $finalKey,
403
            $ttl,
404
            $value
405
        );
406
407
        return $result === 1;
408
    }
409
410
    /**
411
     * Rilascia un lock precedentemente acquisito.
412
     *
413
     * @param string      $key             La chiave del lock da rilasciare.
414
     * @param string|null $connection_name Il nome della connessione opzionale da utilizzare. Se null, verrà utilizzata la connessione predefinita.
415
     */
416
    public function unLock(string $key, ?string $connection_name = null): void
417
    {
418
        $finalKey = $this->getFinalKey($key) . ':semaphore';
419
        $luaScript = <<<'LUA'
420
        redis.call('DEL', KEYS[1]);
421
        LUA;
422
        $this->redis->getRedisConnection($connection_name)->eval(
423
            $luaScript,
424
            1, // Number of keys
425
            $finalKey
426
        );
427
    }
428
}
429