Passed
Push — master ( 84d05b...6bfc92 )
by Francis
03:00 queued 01:18
created

RedisCache::atomicCounter()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 29
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 29
rs 9.7998
cc 4
nc 3
nop 4
1
<?php
2
/** @noinspection PhpComposerExtensionStubsInspection */
3
4
namespace Vectorface\Cache;
5
6
use DateInterval;
7
use Vectorface\Cache\Common\PSR16Util;
8
use Redis;
9
use RedisClient\RedisClient;
10
use Vectorface\Cache\Exception\InvalidArgumentException;
11
12
/**
13
 * A cache implementation using one of two client implementations:
14
 *
15
 * @see https://github.com/cheprasov/php-redis-client
16
 * @see https://github.com/phpredis/phpredis
17
 */
18
class RedisCache implements Cache, AtomicCounter
19
{
20
    use PSR16Util { key as PSR16Key; }
21
22
    /** @var Redis|RedisClient */
23
    private $redis;
24
25
    private string $prefix;
26
27
    /**
28
     * RedisCache constructor.
29
     *
30
     * @param $redis
31
     * @param string $prefix
32
     */
33
    public function __construct($redis, string $prefix = '')
34
    {
35
        if (!($redis instanceof Redis || $redis instanceof RedisClient)) {
36
            throw new InvalidArgumentException("Unsupported Redis implementation");
37
        }
38
39
        $this->redis = $redis;
40
        $this->prefix = $prefix;
41
    }
42
43
    /**
44
     * @inheritDoc Vectorface\Cache\Cache
45
     */
46
    public function get(string $key, mixed $default = null) : mixed
47
    {
48
        $result = $this->redis->get($this->key($key));
49
50
        // Not found is 'false' in phpredis, 'null' in php-redis-client
51
        $notFoundResult = ($this->redis instanceof Redis) ? false : null;
52
53
        return ($result !== $notFoundResult) ? $result : $default;
54
    }
55
56
    /**
57
     * @inheritDoc Vectorface\Cache\Cache
58
     */
59
    public function set(string $key, mixed $value, DateInterval|int|null $ttl = null) : bool
60
    {
61
        $ttl = $this->ttl($ttl);
62
63
        // The setex function doesn't support null TTL, so we use set instead
64
        if ($ttl === null) {
0 ignored issues
show
introduced by
The condition $ttl === null is always false.
Loading history...
65
            return $this->redis->set($this->key($key), $value);
66
        }
67
68
        return $this->redis->setex($this->key($key), $ttl, $value);
69
    }
70
71
    /**
72
     * @inheritDoc Vectorface\Cache\Cache
73
     */
74
    public function delete(string $key) : bool
75
    {
76
        return (bool)$this->redis->del($this->key($key));
77
    }
78
79
    /**
80
     * @inheritDoc Vectorface\Cache\Cache
81
     */
82
    public function clean() : bool
83
    {
84
        return true; /* redis does this on its own */
85
    }
86
87
    /**
88
     * @inheritDoc Vectorface\Cache\Cache
89
     */
90
    public function flush() : bool
91
    {
92
        if ($this->redis instanceof Redis) {
93
            return (bool)$this->redis->flushDB();
94
        }
95
96
        return (bool)$this->redis->flushdb(); // We probably don't actually want to do this
97
    }
98
99
    /**
100
     * @inheritDoc Vectorface\Cache\Cache
101
     */
102
    public function clear() : bool
103
    {
104
        return $this->flush();
105
    }
106
107
    /**
108
     * @inheritDoc Vectorface\Cache\Cache
109
     */
110
    public function has(string $key) : bool
111
    {
112
        return (bool)$this->redis->exists($this->key($key));
113
    }
114
115
    /**
116
     * @inheritDoc Vectorface\Cache\Cache
117
     */
118
    public function getMultiple(iterable $keys, mixed $default = null) : iterable
119
    {
120
        $keys = $this->keys($keys);
121
122
        // Some redis client impls don't work with empty args, so return early.
123
        if (empty($keys)) {
124
            return [];
125
        }
126
127
        $values = $this->redis->mget($keys);
128
        // var_dump("Keys: " . json_encode($keys));
129
        // var_dump("Values: " . json_encode($values));
130
131
        $results = [];
132
        foreach ($keys as $index => $key) {
133
            if (!isset($values[$index]) || $values[$index] === false) {
134
                $results[$key] = $default;
135
            } else {
136
                $results[$key] = $values[$index];
137
            }
138
        }
139
        // var_dump("Results: " . json_encode($results));
140
        // echo "\n\n";
141
142
        return $results;
143
    }
144
145
    /**
146
     * @inheritDoc Vectorface\Cache\Cache
147
     */
148
    public function setMultiple(iterable $values, DateInterval|int|null $ttl = null) : bool
149
    {
150
        $ttl = $this->ttl($ttl);
151
152
        // We can't use mset because there's no msetex for expiry,
153
        // so we use multi-exec instead.
154
        $this->redis->multi();
155
156
        foreach ($this->values($values) as $key => $value) {
157
            // Null or TTLs under 1 aren't supported, so we need to just use set in that case.
158
            if ($ttl === null || $ttl < 1) {
159
                $this->redis->set($key, $value);
160
            } else {
161
                $this->redis->setex($key, $ttl, $value);
162
            }
163
        }
164
165
        $results = $this->redis->exec();
166
167
        foreach ($results as $result) {
168
            if ($result === false) {
169
                // @codeCoverageIgnoreStart
170
                return false;
171
                // @codeCoverageIgnoreEnd
172
            }
173
        }
174
175
        return true;
176
    }
177
178
    /**
179
     * @inheritDoc Vectorface\Cache\Cache
180
     */
181
    public function deleteMultiple(iterable $keys) : bool
182
    {
183
        if (empty($keys)) {
184
            return true;
185
        }
186
187
        return (bool)$this->redis->del($this->keys($keys));
188
    }
189
190
    /**
191
     * @inheritdoc AtomicCounter
192
     */
193
    public function increment(string $key, int $step = 1, DateInterval|int|null $ttl = null) : int|false
194
    {
195
        return $this->atomicCounter('incrby', $key, $step, $ttl);
196
    }
197
198
    /**
199
     * @inheritdoc AtomicCounter
200
     */
201
    public function decrement(string $key, int $step = 1, DateInterval|int|null $ttl = null) : int|false
202
    {
203
        return $this->atomicCounter('decrby', $key, $step, $ttl);
204
    }
205
206
    private function atomicCounter(string $method, string $key, int $step = 1, DateInterval|int|null $ttl = null) : int|false
207
    {
208
        $ttl = $this->ttl($ttl);
209
        $key = $this->key($key);
210
        $step = $this->step($step);
211
212
        // We can't just use incrby/decrby because it doesn't support expiry,
213
        // so we use multi-exec instead.
214
        $this->redis->multi();
215
216
        // Set only if the key does not exist (safely sets expiry only if doesn't exist).
217
        // The two redis clients have different advanced set APIs for this.
218
        // They also don't support null or TTLs under 1, so we need to just use setnx in that case.
219
        if ($ttl === null || $ttl < 1) {
220
            $this->redis->setnx($key, 0);
221
        } else {
222
            if ($this->redis instanceof Redis) {
223
                $this->redis->set($key, 0, ['NX', 'EX' => $ttl]);
224
            } else {
225
                $this->redis->set($key, 0, $ttl, null, 'NX');
226
            }
227
        }
228
229
        $this->redis->{$method}($key, $step);
230
231
        $result = $this->redis->exec();
232
233
        // Since we ran two commands, the 1 index should be the incrby/decrby result
234
        return $result[1] ?? false;
235
    }
236
237
    /**
238
     * Override of {@see PSR16Util::key} to allow for having a cache prefix
239
     *
240
     * @param mixed $key
241
     * @return string
242
     */
243
    private function key(mixed $key) : string
244
    {
245
        return $this->prefix . $this->PSR16Key($key);
246
    }
247
}
248