Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 164
0.00% covered (danger)
0.00%
0 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
RedisCache
0.00% covered (danger)
0.00%
0 / 164
0.00% covered (danger)
0.00%
0 / 23
6480
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 connect
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 close
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 set
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 add
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 delete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 exists
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 increment
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 decrement
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 rename
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getLike
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 deleteLike
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 updateExpire
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 flush
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 flushAll
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 replace
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 stats
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getThreshold
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __destruct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 build
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 cachify
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
90
 reverseValue
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
182
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\DataStorage\Cache\Connection
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\DataStorage\Cache\Connection;
16
17use phpOMS\DataStorage\Cache\CacheStatus;
18use phpOMS\DataStorage\Cache\CacheType;
19use phpOMS\DataStorage\Cache\Exception\InvalidConnectionConfigException;
20use phpOMS\Stdlib\Base\Exception\InvalidEnumValue;
21
22/**
23 * RedisCache class.
24 *
25 * @package phpOMS\DataStorage\Cache\Connection
26 * @license OMS License 2.0
27 * @link    https://jingga.app
28 * @since   1.0.0
29 */
30final class RedisCache extends ConnectionAbstract
31{
32    /**
33     * {@inheritdoc}
34     */
35    protected string $type = CacheType::REDIS;
36
37    /**
38     * Delimiter for cache meta data
39     *
40     * @var string
41     * @since 1.0.0
42     */
43    private const DELIM = '$';
44
45    /**
46     * Constructor
47     *
48     * @param array{db:int, host:string, port:int} $data Cache data
49     *
50     * @since 1.0.0
51     */
52    public function __construct(array $data)
53    {
54        $this->con = new \Redis();
55        $this->connect($data);
56    }
57
58    /**
59     * Connect to cache
60     *
61     * @param null|array{db:int, host:string, port:int} $data Cache data
62     *
63     * @return void
64     *
65     * @throws InvalidConnectionConfigException
66     *
67     * @since 1.0.0
68     */
69    public function connect(array $data = null) : void
70    {
71        $this->dbdata = isset($data) ? $data : $this->dbdata;
72
73        if (!isset($this->dbdata['host'], $this->dbdata['port'], $this->dbdata['db'])) {
74            $this->status = CacheStatus::FAILURE;
75            throw new InvalidConnectionConfigException((string) \json_encode($this->dbdata));
76        }
77
78        $this->con->connect($this->dbdata['host'], $this->dbdata['port']);
79
80        try {
81            $this->con->ping();
82        } catch (\Throwable $_) {
83            $this->status = CacheStatus::FAILURE;
84            return;
85        }
86
87        $this->con->setOption(\Redis::OPT_SERIALIZER, (string) \Redis::SERIALIZER_NONE);
88        $this->con->setOption(\Redis::OPT_SCAN, (string) \Redis::SCAN_NORETRY);
89        $this->con->select($this->dbdata['db']);
90
91        $this->status = CacheStatus::OK;
92    }
93
94    /**
95     * {@inheritdoc}
96     */
97    public function close() : void
98    {
99        if ($this->con !== null) {
100            $this->con->close();
101        }
102
103        parent::close();
104    }
105
106    /**
107     * {@inheritdoc}
108     */
109    public function set(int | string $key, mixed $value, int $expire = -1) : void
110    {
111        if ($this->status !== CacheStatus::OK) {
112            return;
113        }
114
115        if ($expire > 0) {
116            $this->con->setEx((string) $key, $expire, $this->build($value));
117
118            return;
119        }
120
121        $this->con->set((string) $key, $this->build($value));
122    }
123
124    /**
125     * {@inheritdoc}
126     */
127    public function add(int | string $key, mixed $value, int $expire = -1) : bool
128    {
129        if ($this->status !== CacheStatus::OK) {
130            return false;
131        }
132
133        if ($expire > 0) {
134            return $this->con->setNx((string) $key, $this->build($value), $expire);
135        }
136
137        return $this->con->setNx((string) $key, $this->build($value));
138    }
139
140    /**
141     * {@inheritdoc}
142     */
143    public function get(int | string $key, int $expire = -1) : mixed
144    {
145        if ($this->status !== CacheStatus::OK || $this->con->exists((string) $key) < 1) {
146            return null;
147        }
148
149        $result = $this->con->get((string) $key);
150
151        if (\is_string($result) && ($result[0] ?? null) === self::DELIM) {
152            $result = \substr($result, 1);
153            $type   = (int) $result[0];
154            $start  = (int) \strpos($result, self::DELIM);
155            $result = $this->reverseValue($type, $result, $start);
156        }
157
158        return $result;
159    }
160
161    /**
162     * {@inheritdoc}
163     */
164    public function delete(int | string $key, int $expire = -1) : bool
165    {
166        if ($this->status !== CacheStatus::OK) {
167            return false;
168        }
169
170        $this->con->del((string) $key);
171
172        return true;
173    }
174
175    /**
176     * {@inheritdoc}
177     */
178    public function exists(int | string $key, int $expire = -1) : bool
179    {
180        if ($this->status !== CacheStatus::OK) {
181            return false;
182        }
183
184        return $this->con->exists((string) $key) > 0;
185    }
186
187    /**
188     * {@inheritdoc}
189     */
190    public function increment(int | string $key, int $value = 1) : bool
191    {
192        if ($this->status !== CacheStatus::OK
193            || !$this->exists((string) $key)
194        ) {
195            return false;
196        }
197
198        $this->con->incrBy((string) $key, $value);
199
200        return true;
201    }
202
203    /**
204     * {@inheritdoc}
205     */
206    public function decrement(int | string $key, int $value = 1) : bool
207    {
208        if ($this->status !== CacheStatus::OK
209            || !$this->exists((string) $key)
210        ) {
211            return false;
212        }
213
214        $this->con->decrBy((string) $key, $value);
215
216        return true;
217    }
218
219    /**
220     * {@inheritdoc}
221     */
222    public function rename(int | string $old, int | string $new, int $expire = -1) : bool
223    {
224        if ($this->status !== CacheStatus::OK) {
225            return false;
226        }
227
228        $this->con->rename((string) $old, (string) $new);
229
230        if ($expire > 0) {
231            $this->con->expire((string) $new, $expire);
232        }
233
234        return true;
235    }
236
237    /**
238     * {@inheritdoc}
239     */
240    public function getLike(string $pattern, int $expire = -1) : array
241    {
242        if ($this->status !== CacheStatus::OK) {
243            return [];
244        }
245
246        $keys   = $this->con->keys('*');
247        $values = [];
248
249        foreach ($keys as $key) {
250            if (\preg_match('/' . $pattern . '/', $key) === 1) {
251                $result = $this->con->get((string) $key);
252                if (\is_string($result) && ($result[0] ?? null) === self::DELIM) {
253                    $result = \substr($result, 1);
254                    $type   = (int) $result[0];
255                    $start  = (int) \strpos($result, self::DELIM);
256                    $result = $this->reverseValue($type, $result, $start);
257                }
258
259                $values[] = $result;
260            }
261        }
262
263        return $values;
264    }
265
266    /**
267     * {@inheritdoc}
268     */
269    public function deleteLike(string $pattern, int $expire = -1) : bool
270    {
271        if ($this->status !== CacheStatus::OK) {
272            return false;
273        }
274
275        $keys = $this->con->keys('*');
276        foreach ($keys as $key) {
277            if (\preg_match('/' . $pattern . '/', $key) === 1) {
278                $this->con->del($key);
279            }
280        }
281
282        return true;
283    }
284
285    /**
286     * {@inheritdoc}
287     */
288    public function updateExpire(int | string $key, int $expire = -1) : bool
289    {
290        if ($this->status !== CacheStatus::OK) {
291            return false;
292        }
293
294        if ($expire > 0) {
295            $this->con->expire((string) $key, $expire);
296        }
297
298        return true;
299    }
300
301    /**
302     * {@inheritdoc}
303     */
304    public function flush(int $expire = 0) : bool
305    {
306        return $this->status === CacheStatus::OK;
307    }
308
309    /**
310     * {@inheritdoc}
311     */
312    public function flushAll() : bool
313    {
314        if ($this->status !== CacheStatus::OK) {
315            return false;
316        }
317
318        $this->con->flushDb();
319
320        return true;
321    }
322
323    /**
324     * {@inheritdoc}
325     */
326    public function replace(int | string $key, mixed $value, int $expire = -1) : bool
327    {
328        if ($this->status !== CacheStatus::OK) {
329            return false;
330        }
331
332        if ($this->con->exists((string) $key) > 0) {
333            $this->set((string) $key, $value, $expire);
334
335            return true;
336        }
337
338        return false;
339    }
340
341    /**
342     * {@inheritdoc}
343     */
344    public function stats() : array
345    {
346        if ($this->status !== CacheStatus::OK) {
347            return [];
348        }
349
350        $info = $this->con->info();
351
352        $stats           = [];
353        $stats['status'] = $this->status;
354        $stats['count']  = $this->con->dbSize();
355        $stats['size']   = $info['used_memory'];
356
357        return $stats;
358    }
359
360    /**
361     * {@inheritdoc}
362     */
363    public function getThreshold() : int
364    {
365        return 0;
366    }
367
368    /**
369     * Destructor.
370     *
371     * @since 1.0.0
372     */
373    public function __destruct()
374    {
375        $this->close();
376    }
377
378    /**
379     * Build value
380     *
381     * @param mixed $value Data to cache
382     *
383     * @return mixed
384     *
385     * @since 1.0.0
386     */
387    private function build(mixed $value) : mixed
388    {
389        $type = $this->dataType($value);
390        $raw  = $this->cachify($value, $type);
391
392        return \is_string($raw)
393            && ($type !== CacheValueType::_INT && $type !== CacheValueType::_FLOAT)
394            ? self::DELIM . $type . self::DELIM . $raw
395            : $raw;
396    }
397
398    /**
399     * Create string representation of data for storage
400     *
401     * @param mixed $value Value of the data
402     * @param int   $type  Type of the cache data
403     *
404     * @return mixed
405     *
406     * @throws InvalidEnumValue This exception is thrown if an unsupported cache value type is used
407     *
408     * @since 1.0.0
409     */
410    private function cachify(mixed $value, int $type) : mixed
411    {
412        if ($type === CacheValueType::_INT || $type === CacheValueType::_STRING || $type === CacheValueType::_BOOL) {
413            return (string) $value;
414        } elseif ($type === CacheValueType::_FLOAT) {
415            return \rtrim(\rtrim(\number_format($value, 5, '.', ''), '0'), '.');
416        } elseif ($type === CacheValueType::_ARRAY) {
417            return (string) \json_encode($value);
418        } elseif ($type === CacheValueType::_SERIALIZABLE) {
419            return \get_class($value) . self::DELIM . $value->serialize();
420        } elseif ($type === CacheValueType::_JSONSERIALIZABLE) {
421            return \get_class($value) . self::DELIM . ((string) \json_encode($value->jsonSerialize()));
422        } elseif ($type === CacheValueType::_NULL) {
423            return '';
424        }
425
426        throw new InvalidEnumValue($type);
427    }
428
429    /**
430     * Parse cached value
431     *
432     * @param int   $type  Cached value type
433     * @param mixed $raw   Cached value
434     * @param int   $start Value start position
435     *
436     * @return mixed
437     *
438     * @since 1.0.0
439     */
440    private function reverseValue(int $type, mixed $raw, int $start) : mixed
441    {
442        switch ($type) {
443            case CacheValueType::_INT:
444                return (int) \substr($raw, $start + 1);
445            case CacheValueType::_FLOAT:
446                return (float) \substr($raw, $start + 1);
447            case CacheValueType::_BOOL:
448                return (bool) \substr($raw, $start + 1);
449            case CacheValueType::_STRING:
450                return \substr($raw, $start + 1);
451            case CacheValueType::_ARRAY:
452                $array = \substr($raw, $start + 1);
453                return \json_decode($array === false ? '[]' : $array, true);
454            case CacheValueType::_NULL:
455                return null;
456            case CacheValueType::_JSONSERIALIZABLE:
457                $namespaceStart = (int) \strpos($raw, self::DELIM, $start);
458                $namespaceEnd   = (int) \strpos($raw, self::DELIM, $namespaceStart + 1);
459                $namespace      = \substr($raw, $namespaceStart + 1, $namespaceEnd - $namespaceStart - 1);
460
461                if ($namespace === false) {
462                    return null; // @codeCoverageIgnore
463                }
464
465                return new $namespace();
466            case CacheValueType::_SERIALIZABLE:
467                $namespaceStart = (int) \strpos($raw, self::DELIM, $start);
468                $namespaceEnd   = (int) \strpos($raw, self::DELIM, $namespaceStart + 1);
469                $namespace      = \substr($raw, $namespaceStart + 1, $namespaceEnd - $namespaceStart - 1);
470
471                if ($namespace === false) {
472                    return null; // @codeCoverageIgnore
473                }
474
475                $obj = new $namespace();
476                $obj->unserialize(\substr($raw, $namespaceEnd + 1));
477
478                return $obj;
479            default:
480                return null; // @codeCoverageIgnore
481        }
482    }
483}