Passed
Pull Request — main (#63)
by Sílvio
03:01
created

DatabaseCacheStore::storeCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 2
eloc 6
c 2
b 0
f 1
nc 2
nop 4
dl 0
loc 9
rs 10
1
<?php
2
3
namespace Silviooosilva\CacheerPhp\CacheStore;
4
5
use Silviooosilva\CacheerPhp\Interface\CacheerInterface;
6
use Silviooosilva\CacheerPhp\Helpers\CacheDatabaseHelper;
7
use Silviooosilva\CacheerPhp\Utils\CacheLogger;
8
use Silviooosilva\CacheerPhp\Repositories\CacheDatabaseRepository;
9
use Silviooosilva\CacheerPhp\CacheStore\CacheManager\GenericFlusher;
10
use Silviooosilva\CacheerPhp\Helpers\CacheFileHelper;
11
use Silviooosilva\CacheerPhp\Helpers\FlushHelper;
12
use Silviooosilva\CacheerPhp\Core\Connect;
13
use Silviooosilva\CacheerPhp\Core\MigrationManager;
14
15
/**
16
 * Class DatabaseCacheStore
17
 * @author Sílvio Silva <https://github.com/silviooosilva>
18
 * @package Silviooosilva\CacheerPhp
19
 */
20
class DatabaseCacheStore implements CacheerInterface
21
{
22
    /**
23
     * @var boolean
24
     */
25
    private bool $success = false;
26
27
    /**
28
     * @var string
29
     */
30
    private string $message = '';
31
32
    /**
33
     * @var ?CacheLogger
34
     */
35
    private ?CacheLogger $logger = null;
36
37
    /**
38
     * @var CacheDatabaseRepository
39
     */
40
    private CacheDatabaseRepository $cacheRepository;
41
42
    /** @var int|null */
43
    private ?int $defaultTTL = null;
44
45
    /** @var GenericFlusher|null */
46
    private ?GenericFlusher $flusher = null;
47
48
    /**
49
     * DatabaseCacheStore constructor.
50
     *
51
     * @param string $logPath
52
     * @param array $options
53
     */
54
    public function __construct(string $logPath, array $options = [])
55
    {
56
        $this->logger = new CacheLogger($logPath);
57
        $table = $options['table'] ?? 'cacheer_table';
58
        $this->cacheRepository = new CacheDatabaseRepository($table);
59
60
        // Ensure the custom table exists by running a targeted migration
61
        $pdo = Connect::getInstance();
62
        MigrationManager::migrate($pdo, $table);
63
64
        if (!empty($options['expirationTime'])) {
65
            $this->defaultTTL = (int) CacheFileHelper::convertExpirationToSeconds((string) $options['expirationTime']);
66
        }
67
68
        $lastFlushFile = FlushHelper::pathFor('db', $table);
0 ignored issues
show
Bug introduced by
It seems like $table can also be of type null; however, parameter $identifier of Silviooosilva\CacheerPhp...\FlushHelper::pathFor() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

68
        $lastFlushFile = FlushHelper::pathFor('db', /** @scrutinizer ignore-type */ $table);
Loading history...
69
        $this->flusher = new GenericFlusher($lastFlushFile, function () {
70
            $this->flushCache();
71
        });
72
        $this->flusher->handleAutoFlush($options);
73
    }
74
75
    /**
76
     * Appends data to an existing cache item.
77
     * 
78
     * @param string $cacheKey
79
     * @param mixed  $cacheData
80
     * @param string $namespace
81
     * @return bool
82
     */
83
    public function appendCache(string $cacheKey, mixed $cacheData, string $namespace = ''): bool
84
    {
85
        $currentCacheData = $this->getCache($cacheKey, $namespace);
86
        $mergedCacheData = CacheDatabaseHelper::arrayIdentifier($currentCacheData, $cacheData);
87
88
        if ($this->updateCache($cacheKey, $mergedCacheData, $namespace)) {
89
            $this->logger->debug("{$this->getMessage()} from database driver.");
0 ignored issues
show
Bug introduced by
The method debug() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

89
            $this->logger->/** @scrutinizer ignore-call */ 
90
                           debug("{$this->getMessage()} from database driver.");

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
90
            return true;
91
        }
92
93
        $this->logger->error("{$this->getMessage()} from database driver.");
94
        return false;
95
    }
96
97
    /**
98
     * Clears a specific cache item.
99
     * 
100
     * @param string $cacheKey
101
     * @param string $namespace
102
     * @return void
103
     */
104
    public function clearCache(string $cacheKey, string $namespace = ''): void
105
    {
106
        $data = $this->cacheRepository->clear($cacheKey, $namespace);
107
        if($data) {
108
            $this->setMessage("Cache deleted successfully!", true);
109
        } else {
110
            $this->setMessage("Cache does not exists!", false);
111
        }
112
113
        $this->logger->debug("{$this->getMessage()} from database driver.");
114
    }
115
116
    /**
117
     * Flushes all cache items.
118
     * 
119
     * @return void
120
     */
121
    public function flushCache(): void
122
    {
123
        if($this->cacheRepository->flush()){
124
            $this->setMessage("Flush finished successfully", true);
125
        } else {
126
            $this->setMessage("Something went wrong. Please, try again.", false);
127
        }
128
129
        $this->logger->info("{$this->getMessage()} from database driver.");
130
131
    }
132
133
    /**
134
     * Associates one or more keys to a tag using a reserved namespace.
135
     *
136
     * @param string $tag
137
     * @param string ...$keys
138
     * @return bool
139
     */
140
    public function tag(string $tag, string ...$keys): bool
141
    {
142
        $indexKey = "tag:" . $tag;
143
        $namespace = '__tags__';
144
        $existing = $this->cacheRepository->retrieve($indexKey, $namespace) ?? [];
145
        if (!is_array($existing)) {
146
            $existing = [];
147
        }
148
        foreach ($keys as $key) {
149
            // Store either raw key or "namespace:key"
150
            $existing[$key] = true;
151
        }
152
        $ok = $this->cacheRepository->store($indexKey, $existing, $namespace, 31536000);
153
        $this->setMessage($ok ? "Tagged successfully" : "Failed to tag keys", $ok);
154
        $this->logger->debug("{$this->getMessage()} from database driver.");
155
        return $ok;
156
    }
157
158
    /**
159
     * Flush all keys associated with a tag.
160
     *
161
     * @param string $tag
162
     * @return void
163
     */
164
    public function flushTag(string $tag): void
165
    {
166
        $indexKey = "tag:" . $tag;
167
        $namespace = '__tags__';
168
        $existing = $this->cacheRepository->retrieve($indexKey, $namespace) ?? [];
169
        if (is_array($existing)) {
170
            foreach (array_keys($existing) as $key) {
171
                if (str_contains($key, ':')) {
172
                    [$np, $k] = explode(':', $key, 2);
173
                    $this->clearCache($k, $np);
174
                } else {
175
                    $this->clearCache($key, '');
176
                }
177
            }
178
        }
179
        $this->cacheRepository->clear($indexKey, $namespace);
180
        $this->setMessage("Tag flushed successfully", true);
181
        $this->logger->debug("{$this->getMessage()} from database driver.");
182
    }
183
184
    /**
185
     * Gets a single cache item.
186
     * 
187
     * @param string $cacheKey
188
     * @param string $namespace
189
     * @param string|int $ttl
190
     * @return mixed
191
     */
192
    public function getCache(string $cacheKey, string $namespace = '', string|int $ttl = 3600): mixed
193
    {
194
        $cacheData = $this->retrieveCache($cacheKey, $namespace);
195
        if ($cacheData) {
196
            $this->setMessage("Cache retrieved successfully", true);
197
            $this->logger->debug("{$this->getMessage()} from database driver.");
198
            return $cacheData;
199
        }
200
        $this->setMessage("CacheData not found, does not exists or expired", false);
201
        $this->logger->info("{$this->getMessage()} from database driver.");
202
        return null;
203
    }
204
205
    /**
206
     * Gets all items in a specific namespace.
207
     * 
208
     * @param string $namespace
209
     * @return array
210
     */
211
    public function getAll(string $namespace = ''): array
212
    {
213
        $cacheData = $this->cacheRepository->getAll($namespace);
214
        if ($cacheData) {
215
            $this->setMessage("Cache retrieved successfully", true);
216
            $this->logger->debug("{$this->getMessage()} from database driver.");
217
            return $cacheData;
218
        }
219
        $this->setMessage("No cache data found for the provided namespace", false);
220
        $this->logger->info("{$this->getMessage()} from database driver.");
221
        return [];
222
    }
223
224
    /**
225
     * Retrieves multiple cache items by their keys.
226
     * 
227
     * @param array  $cacheKeys
228
     * @param string $namespace
229
     * @param string|int $ttl
230
     * @return array
231
     */
232
    public function getMany(array $cacheKeys, string $namespace = '', string|int $ttl = 3600): array
233
    {
234
        $cacheData = [];
235
        foreach ($cacheKeys as $cacheKey) {
236
            $data = $this->getCache($cacheKey, $namespace, $ttl);
237
            if ($data) {
238
                $cacheData[$cacheKey] = $data;
239
            }
240
        }
241
        if (!empty($cacheData)) {
242
            $this->setMessage("Cache retrieved successfully", true);
243
            $this->logger->debug("{$this->getMessage()} from database driver.");
244
            return $cacheData;
245
        }
246
        $this->setMessage("No cache data found for the provided keys", false);
247
        $this->logger->info("{$this->getMessage()} from database driver.");
248
        return [];
249
    }
250
251
    /**
252
     * Checks if a cache item exists.
253
     * 
254
     * @return string
255
     */
256
    public function getMessage(): string
257
    {
258
        return $this->message;
259
    }
260
261
    /**
262
     * Checks if a cache item exists.
263
     * 
264
     * @param string $cacheKey
265
     * @param string $namespace
266
     * @return bool
267
     */
268
    public function has(string $cacheKey, string $namespace = ''): bool
269
    {
270
        $cacheData = $this->getCache($cacheKey, $namespace);
271
272
        if ($cacheData) {
273
            $this->setMessage("Cache key: {$cacheKey} exists and it's available from database driver.", true);
274
            $this->logger->debug("{$this->getMessage()}");
275
            return true;
276
        }
277
278
        $this->setMessage("Cache key: {$cacheKey} does not exist or it's expired from database driver.", false);
279
        $this->logger->debug("{$this->getMessage()}");
280
281
        return false;
282
    }
283
284
    /**
285
     * Checks if the last operation was successful.
286
     * 
287
     * @return boolean
288
     */
289
    public function isSuccess(): bool
290
    {
291
        return $this->success;
292
    }
293
294
    /**
295
     * Store multiple items in the cache.
296
     * 
297
     * @param array   $items
298
     * @param string  $namespace
299
     * @param integer $batchSize
300
     * @return void
301
     */
302
    public function putMany(array $items, string $namespace = '', int $batchSize = 100): void
303
    {
304
        $processedCount = 0;
305
        $itemCount = count($items);
306
        while ($processedCount < $itemCount) {
307
            $batchItems = array_slice($items, $processedCount, $batchSize);
308
            $this->processBatchItems($batchItems, $namespace);
309
            $processedCount += count($batchItems);
310
        }
311
    }
312
313
    /**
314
     * Stores an item in the cache with a specific TTL.
315
     * 
316
     * @param string $cacheKey
317
     * @param mixed  $cacheData
318
     * @param string $namespace
319
     * @param string|int $ttl
320
     * @return bool
321
     */
322
    public function putCache(string $cacheKey, mixed $cacheData, string $namespace = '', string|int $ttl = 3600): bool
323
    {
324
        $ttlToUse = $ttl;
325
        if ($this->defaultTTL !== null && ($ttl === null || (int)$ttl === 3600)) {
326
            $ttlToUse = $this->defaultTTL;
327
        }
328
        if (is_string($ttlToUse)) {
329
            $ttlToUse = (int) CacheFileHelper::convertExpirationToSeconds($ttlToUse);
330
        }
331
332
        if($this->storeCache($cacheKey, $cacheData, $namespace, $ttlToUse)){
333
            $this->logger->debug("{$this->getMessage()} from database driver.");
334
            return true;
335
        }
336
        $this->logger->error("{$this->getMessage()} from database driver.");
337
        return false;
338
    }
339
340
    /**
341
     * Renews the cache for a specific key with a new TTL.
342
     * 
343
     * @param string $cacheKey
344
     * @param string|int $ttl
345
     * @param string $namespace
346
     * @return void
347
     */
348
    public function renewCache(string $cacheKey, int | string $ttl, string $namespace = ''): void
349
    {
350
        $cacheData = $this->getCache($cacheKey, $namespace);
351
        if ($cacheData) {
352
            $this->renew($cacheKey, $ttl, $namespace);
353
            $this->setMessage("Cache with key {$cacheKey} renewed successfully", true);
354
            $this->logger->debug("{$this->getMessage()} from database driver.");
355
        }
356
    }
357
358
    /**
359
     * Processes a batch of cache items.
360
     * 
361
     * @param array  $batchItems
362
     * @param string $namespace
363
     * @return void
364
     */
365
    private function processBatchItems(array $batchItems, string $namespace): void
366
    {
367
        foreach($batchItems as $item) {
368
            CacheDatabaseHelper::validateCacheItem($item);
369
            $cacheKey = $item['cacheKey'];
370
            $cacheData = $item['cacheData'];
371
            $mergedData = CacheDatabaseHelper::mergeCacheData($cacheData);
372
            $this->putCache($cacheKey, $mergedData, $namespace);
373
        }
374
    }
375
376
    /**
377
     * Renews the expiration time of a cache item.
378
     * 
379
     * @param string $cacheKey
380
     * @param string|int $ttl
381
     * @param string $namespace
382
     * @return bool
383
     */
384
    private function renew(string $cacheKey, string|int $ttl = 3600, string $namespace = ''): bool
385
    {
386
        $cacheData = $this->getCache($cacheKey, $namespace);
387
        if ($cacheData) {
388
            $renewedCache = $this->cacheRepository->renew($cacheKey, $ttl, $namespace);
389
            if ($renewedCache) {
390
                $this->setMessage("Cache with key {$cacheKey} renewed successfully", true);
391
                $this->logger->debug("{$this->getMessage()} from database driver.");
392
                return true;
393
            }
394
            return false;
395
        }
396
        return false;
397
    }
398
399
    /**
400
     * Sets a message and its success status.
401
     * 
402
     * @param string  $message
403
     * @param boolean $success
404
     * @return void
405
     */
406
    private function setMessage(string $message, bool $success): void
407
    {
408
        $this->message = $message;
409
        $this->success = $success;
410
    }
411
412
    /**
413
     * Retrieves a cache item by its key.
414
     * @param string $cacheKey
415
     * @param string $namespace
416
     * @return mixed
417
     */
418
    private function retrieveCache(string $cacheKey, string $namespace = ''): mixed
419
    {
420
        return $this->cacheRepository->retrieve($cacheKey, $namespace);
421
    }
422
423
    /**
424
     * Stores a cache item.
425
     *
426
     * @param string $cacheKey
427
     * @param mixed $cacheData
428
     * @param string $namespace
429
     * @param string|int $ttl
430
     * @return bool
431
     */
432
    private function storeCache(string $cacheKey, mixed $cacheData, string $namespace = '', string|int $ttl = 3600): bool
433
    {
434
        $data = $this->cacheRepository->store($cacheKey, $cacheData, $namespace, $ttl);
435
        if($data) {
436
            $this->setMessage("Cache Stored Successfully", true);
437
            return true;
438
        }
439
        $this->setMessage("Already exists a cache with this key...", false);
440
        return false;
441
    }
442
443
    /**
444
     * Updates an existing cache item.
445
     * 
446
     * @param string $cacheKey
447
     * @param mixed  $cacheData
448
     * @param string $namespace
449
     * @return bool
450
     */
451
    private function updateCache(string $cacheKey, mixed $cacheData, string $namespace = ''): bool
452
    {
453
        $data = $this->cacheRepository->update($cacheKey, $cacheData, $namespace);
454
        if($data) {
455
            $this->setMessage("Cache updated successfully.", true);
456
            return true;
457
        }
458
        $this->setMessage("Cache does not exist or update failed!", false);
459
        return false;
460
    }
461
}
462