Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 164 |
|
0.00% |
0 / 23 |
CRAP | |
0.00% |
0 / 1 |
RedisCache | |
0.00% |
0 / 164 |
|
0.00% |
0 / 23 |
6480 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
connect | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
close | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
set | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
add | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
get | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
delete | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
exists | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
increment | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
decrement | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
rename | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getLike | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
deleteLike | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
updateExpire | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
flush | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
flushAll | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
replace | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
stats | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
getThreshold | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__destruct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
build | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
cachify | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
90 | |||
reverseValue | |
0.00% |
0 / 27 |
|
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 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\DataStorage\Cache\Connection; |
16 | |
17 | use phpOMS\DataStorage\Cache\CacheStatus; |
18 | use phpOMS\DataStorage\Cache\CacheType; |
19 | use phpOMS\DataStorage\Cache\Exception\InvalidConnectionConfigException; |
20 | use 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 | */ |
30 | final 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 | } |