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