Passed
Branch main (8837c4)
by Sílvio
14:13
created

DatabaseCacheStore::appendCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 2
eloc 7
c 2
b 0
f 1
nc 2
nop 3
dl 0
loc 12
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\Enums\CacheStoreType;
13
use Silviooosilva\CacheerPhp\Core\Connect;
14
use Silviooosilva\CacheerPhp\Core\MigrationManager;
15
16
/**
17
 * Class DatabaseCacheStore
18
 * @author Sílvio Silva <https://github.com/silviooosilva>
19
 * @package Silviooosilva\CacheerPhp
20
 */
21
class DatabaseCacheStore implements CacheerInterface
22
{
23
    /**
24
     * @var boolean
25
     */
26
    private bool $success = false;
27
28
    /**
29
     * @var string
30
     */
31
    private string $message = '';
32
33
    /**
34
     * @var ?CacheLogger
35
     */
36
    private ?CacheLogger $logger = null;
37
38
    /**
39
     * @var CacheDatabaseRepository
40
     */
41
    private CacheDatabaseRepository $cacheRepository;
42
43
    /** @var int|null */
44
    private ?int $defaultTTL = null;
45
46
    /** @var GenericFlusher|null */
47
    private ?GenericFlusher $flusher = null;
48
49
    /**
50
     * DatabaseCacheStore constructor.
51
     *
52
     * @param string $logPath
53
     * @param array $options
54
     */
55
    public function __construct(string $logPath, array $options = [])
56
    {
57
        $this->logger = new CacheLogger($logPath);
58
        $tableOption = $options['table'] ?? 'cacheer_table';
59
        $table = is_string($tableOption) && $tableOption !== '' ? $tableOption : 'cacheer_table';
60
        $this->cacheRepository = new CacheDatabaseRepository($table);
61
62
        // Ensure the custom table exists by running a targeted migration
63
        $pdo = Connect::getInstance();
64
        MigrationManager::migrate($pdo, $table);
65
66
        if (!empty($options['expirationTime'])) {
67
            $this->defaultTTL = (int) CacheFileHelper::convertExpirationToSeconds((string) $options['expirationTime']);
68
        }
69
70
        $lastFlushFile = FlushHelper::pathFor(CacheStoreType::DATABASE, $table);
71
        $this->flusher = new GenericFlusher($lastFlushFile, function () {
72
            $this->flushCache();
73
        });
74
        $this->flusher->handleAutoFlush($options);
75
    }
76
77
    /**
78
     * Appends data to an existing cache item.
79
     * 
80
     * @param string $cacheKey
81
     * @param mixed  $cacheData
82
     * @param string $namespace
83
     * @return bool
84
     */
85
    public function appendCache(string $cacheKey, mixed $cacheData, string $namespace = ''): bool
86
    {
87
        $currentCacheData = $this->getCache($cacheKey, $namespace);
88
        $mergedCacheData = CacheDatabaseHelper::arrayIdentifier($currentCacheData, $cacheData);
89
90
        if ($this->updateCache($cacheKey, $mergedCacheData, $namespace)) {
91
            $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

91
            $this->logger->/** @scrutinizer ignore-call */ 
92
                           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...
92
            return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the return type mandated by Silviooosilva\CacheerPhp...nterface::appendCache() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
93
        }
94
95
        $this->logger->error("{$this->getMessage()} from database driver.");
96
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by Silviooosilva\CacheerPhp...nterface::appendCache() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
97
    }
98
99
    /**
100
     * Clears a specific cache item.
101
     * 
102
     * @param string $cacheKey
103
     * @param string $namespace
104
     * @return void
105
     */
106
    public function clearCache(string $cacheKey, string $namespace = ''): void
107
    {
108
        $data = $this->cacheRepository->clear($cacheKey, $namespace);
109
        if($data) {
110
            $this->setMessage("Cache deleted successfully!", true);
111
        } else {
112
            $this->setMessage("Cache does not exists!", false);
113
        }
114
115
        $this->logger->debug("{$this->getMessage()} from database driver.");
116
    }
117
118
    /**
119
     * Flushes all cache items.
120
     * 
121
     * @return void
122
     */
123
    public function flushCache(): void
124
    {
125
        if($this->cacheRepository->flush()){
126
            $this->setMessage("Flush finished successfully", true);
127
        } else {
128
            $this->setMessage("Something went wrong. Please, try again.", false);
129
        }
130
131
        $this->logger->info("{$this->getMessage()} from database driver.");
132
133
    }
134
135
    /**
136
     * Associates one or more keys to a tag using a reserved namespace.
137
     *
138
     * @param string $tag
139
     * @param string ...$keys
140
     * @return bool
141
     */
142
    public function tag(string $tag, string ...$keys): bool
143
    {
144
        $indexKey = "tag:" . $tag;
145
        $namespace = '__tags__';
146
        $existing = $this->cacheRepository->retrieve($indexKey, $namespace) ?? [];
147
        if (!is_array($existing)) {
148
            $existing = [];
149
        }
150
        foreach ($keys as $key) {
151
            // Store either raw key or "namespace:key"
152
            $existing[$key] = true;
153
        }
154
        $ok = $this->cacheRepository->store($indexKey, $existing, $namespace, 31536000);
155
        $this->setMessage($ok ? "Tagged successfully" : "Failed to tag keys", $ok);
156
        $this->logger->debug("{$this->getMessage()} from database driver.");
157
        return $ok;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ok returns the type boolean which is incompatible with the return type mandated by Silviooosilva\CacheerPhp...heStoreInterface::tag() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

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