Completed
Push — main ( 282277...c3128d )
by
unknown
20s queued 16s
created

SuperCacheManager::getOriginalKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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