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

SuperCacheManager::getFinalTag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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