Passed
Pull Request — master (#8)
by Francis
09:06 queued 07:39
created

RedisCache::getMultiple()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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