Completed
Push — master ( 1c122d...90440a )
by Mark
19:01 queued 15:35
created

RedisEngine::clear()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 3
nop 1
dl 0
loc 27
rs 8.8657
c 0
b 0
f 0
1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         2.2.0
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
16
namespace Cake\Cache\Engine;
17
18
use Cake\Cache\CacheEngine;
19
use Redis;
20
use RedisException;
21
22
/**
23
 * Redis storage engine for cache.
24
 */
25
class RedisEngine extends CacheEngine
26
{
27
28
    /**
29
     * Redis wrapper.
30
     *
31
     * @var \Redis
32
     */
33
    protected $_Redis;
34
35
    /**
36
     * The default config used unless overridden by runtime configuration
37
     *
38
     * - `database` database number to use for connection.
39
     * - `duration` Specify how long items in this cache configuration last.
40
     * - `groups` List of groups or 'tags' associated to every key stored in this config.
41
     *    handy for deleting a complete group from cache.
42
     * - `password` Redis server password.
43
     * - `persistent` Connect to the Redis server with a persistent connection
44
     * - `port` port number to the Redis server.
45
     * - `prefix` Prefix appended to all entries. Good for when you need to share a keyspace
46
     *    with either another cache config or another application.
47
     * - `probability` Probability of hitting a cache gc cleanup. Setting to 0 will disable
48
     *    cache::gc from ever being called automatically.
49
     * - `server` URL or ip to the Redis server host.
50
     * - `timeout` timeout in seconds (float).
51
     * - `unix_socket` Path to the unix socket file (default: false)
52
     *
53
     * @var array
54
     */
55
    protected $_defaultConfig = [
56
        'database' => 0,
57
        'duration' => 3600,
58
        'groups' => [],
59
        'password' => false,
60
        'persistent' => true,
61
        'port' => 6379,
62
        'prefix' => 'cake_',
63
        'probability' => 100,
64
        'host' => null,
65
        'server' => '127.0.0.1',
66
        'timeout' => 0,
67
        'unix_socket' => false,
68
    ];
69
70
    /**
71
     * Initialize the Cache Engine
72
     *
73
     * Called automatically by the cache frontend
74
     *
75
     * @param array $config array of setting for the engine
76
     * @return bool True if the engine has been successfully initialized, false if not
77
     */
78
    public function init(array $config = [])
79
    {
80
        if (!extension_loaded('redis')) {
81
            return false;
82
        }
83
84
        if (!empty($config['host'])) {
85
            $config['server'] = $config['host'];
86
        }
87
88
        parent::init($config);
89
90
        return $this->_connect();
91
    }
92
93
    /**
94
     * Connects to a Redis server
95
     *
96
     * @return bool True if Redis server was connected
97
     */
98
    protected function _connect()
99
    {
100
        try {
101
            $this->_Redis = new Redis();
102
            if (!empty($this->_config['unix_socket'])) {
103
                $return = $this->_Redis->connect($this->_config['unix_socket']);
104
            } elseif (empty($this->_config['persistent'])) {
105
                $return = $this->_Redis->connect($this->_config['server'], $this->_config['port'], $this->_config['timeout']);
106
            } else {
107
                $persistentId = $this->_config['port'] . $this->_config['timeout'] . $this->_config['database'];
108
                $return = $this->_Redis->pconnect($this->_config['server'], $this->_config['port'], $this->_config['timeout'], $persistentId);
109
            }
110
        } catch (RedisException $e) {
111
            return false;
112
        }
113
        if ($return && $this->_config['password']) {
114
            $return = $this->_Redis->auth($this->_config['password']);
115
        }
116
        if ($return) {
117
            $return = $this->_Redis->select($this->_config['database']);
118
        }
119
120
        return $return;
121
    }
122
123
    /**
124
     * Write data for key into cache.
125
     *
126
     * @param string $key Identifier for the data
127
     * @param mixed $value Data to be cached
128
     * @return bool True if the data was successfully cached, false on failure
129
     */
130 View Code Duplication
    public function write($key, $value)
131
    {
132
        $key = $this->_key($key);
133
134
        if (!is_int($value)) {
135
            $value = serialize($value);
136
        }
137
138
        $duration = $this->_config['duration'];
139
        if ($duration === 0) {
140
            return $this->_Redis->set($key, $value);
141
        }
142
143
        return $this->_Redis->setEx($key, $duration, $value);
144
    }
145
146
    /**
147
     * Read a key from the cache
148
     *
149
     * @param string $key Identifier for the data
150
     * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it
151
     */
152
    public function read($key)
153
    {
154
        $key = $this->_key($key);
155
156
        $value = $this->_Redis->get($key);
157
        if (preg_match('/^[-]?\d+$/', $value)) {
158
            return (int)$value;
159
        }
160
        if ($value !== false && is_string($value)) {
161
            return unserialize($value);
162
        }
163
164
        return $value;
165
    }
166
167
    /**
168
     * Increments the value of an integer cached key & update the expiry time
169
     *
170
     * @param string $key Identifier for the data
171
     * @param int $offset How much to increment
172
     * @return bool|int New incremented value, false otherwise
173
     */
174 View Code Duplication
    public function increment($key, $offset = 1)
175
    {
176
        $duration = $this->_config['duration'];
177
        $key = $this->_key($key);
178
179
        $value = (int)$this->_Redis->incrBy($key, $offset);
180
        if ($duration > 0) {
181
            $this->_Redis->setTimeout($key, $duration);
182
        }
183
184
        return $value;
185
    }
186
187
    /**
188
     * Decrements the value of an integer cached key & update the expiry time
189
     *
190
     * @param string $key Identifier for the data
191
     * @param int $offset How much to subtract
192
     * @return bool|int New decremented value, false otherwise
193
     */
194 View Code Duplication
    public function decrement($key, $offset = 1)
195
    {
196
        $duration = $this->_config['duration'];
197
        $key = $this->_key($key);
198
199
        $value = (int)$this->_Redis->decrBy($key, $offset);
200
        if ($duration > 0) {
201
            $this->_Redis->setTimeout($key, $duration);
202
        }
203
204
        return $value;
205
    }
206
207
    /**
208
     * Delete a key from the cache
209
     *
210
     * @param string $key Identifier for the data
211
     * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed
212
     */
213
    public function delete($key)
214
    {
215
        $key = $this->_key($key);
216
217
        return $this->_Redis->del($key) > 0;
218
    }
219
220
    /**
221
     * Delete all keys from the cache
222
     *
223
     * @param bool $check If true will check expiration, otherwise delete all.
224
     * @return bool True if the cache was successfully cleared, false otherwise
225
     */
226
    public function clear($check)
227
    {
228
        if ($check) {
229
            return true;
230
        }
231
232
        $this->_Redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);
233
234
        $isAllDeleted = true;
235
        $iterator = null;
236
        $pattern = $this->_config['prefix'] . '*';
237
238
        while (true) {
239
            $keys = $this->_Redis->scan($iterator, $pattern);
240
241
            if ($keys === false) {
242
                break;
243
            }
244
245
            foreach ($keys as $key) {
0 ignored issues
show
Bug introduced by
The expression $keys of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
246
                $isDeleted = ($this->_Redis->del($key) > 0);
247
                $isAllDeleted = $isAllDeleted && $isDeleted;
248
            }
249
        }
250
251
        return $isAllDeleted;
252
    }
253
254
    /**
255
     * Write data for key into cache if it doesn't exist already.
256
     * If it already exists, it fails and returns false.
257
     *
258
     * @param string $key Identifier for the data.
259
     * @param mixed $value Data to be cached.
260
     * @return bool True if the data was successfully cached, false on failure.
261
     * @link https://github.com/phpredis/phpredis#setnx
262
     */
263 View Code Duplication
    public function add($key, $value)
264
    {
265
        $duration = $this->_config['duration'];
266
        $key = $this->_key($key);
267
268
        if (!is_int($value)) {
269
            $value = serialize($value);
270
        }
271
272
        // setNx() doesn't have an expiry option, so follow up with an expiry
273
        if ($this->_Redis->setNx($key, $value)) {
274
            return $this->_Redis->setTimeout($key, $duration);
275
        }
276
277
        return false;
278
    }
279
280
    /**
281
     * Returns the `group value` for each of the configured groups
282
     * If the group initial value was not found, then it initializes
283
     * the group accordingly.
284
     *
285
     * @return array
286
     */
287
    public function groups()
288
    {
289
        $result = [];
290
        foreach ($this->_config['groups'] as $group) {
291
            $value = $this->_Redis->get($this->_config['prefix'] . $group);
292 View Code Duplication
            if (!$value) {
293
                $value = 1;
294
                $this->_Redis->set($this->_config['prefix'] . $group, $value);
295
            }
296
            $result[] = $group . $value;
297
        }
298
299
        return $result;
300
    }
301
302
    /**
303
     * Increments the group value to simulate deletion of all keys under a group
304
     * old values will remain in storage until they expire.
305
     *
306
     * @param string $group name of the group to be cleared
307
     * @return bool success
308
     */
309
    public function clearGroup($group)
310
    {
311
        return (bool)$this->_Redis->incr($this->_config['prefix'] . $group);
312
    }
313
314
    /**
315
     * Disconnects from the redis server
316
     */
317
    public function __destruct()
318
    {
319
        if (empty($this->_config['persistent']) && $this->_Redis instanceof Redis) {
320
            $this->_Redis->close();
321
        }
322
    }
323
}
324