1: <?php
2: /**
3: * Redis storage engine for cache
4: *
5: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
6: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
7: *
8: * Licensed under The MIT License
9: * For full copyright and license information, please see the LICENSE.txt
10: * Redistributions of files must retain the above copyright notice.
11: *
12: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
13: * @link https://cakephp.org CakePHP(tm) Project
14: * @package Cake.Cache.Engine
15: * @since CakePHP(tm) v 2.2
16: * @license https://opensource.org/licenses/mit-license.php MIT License
17: */
18:
19: /**
20: * Redis storage engine for cache.
21: *
22: * @package Cake.Cache.Engine
23: */
24: class RedisEngine extends CacheEngine {
25:
26: /**
27: * Redis wrapper.
28: *
29: * @var Redis
30: */
31: protected $_Redis = null;
32:
33: /**
34: * Settings
35: *
36: * - server = string URL or ip to the Redis server host
37: * - database = integer database number to use for connection
38: * - port = integer port number to the Redis server (default: 6379)
39: * - timeout = float timeout in seconds (default: 0)
40: * - persistent = boolean Connects to the Redis server with a persistent connection (default: true)
41: * - unix_socket = path to the unix socket file (default: false)
42: *
43: * @var array
44: */
45: public $settings = array();
46:
47: /**
48: * Initialize the Cache Engine
49: *
50: * Called automatically by the cache frontend
51: * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array());
52: *
53: * @param array $settings array of setting for the engine
54: * @return bool True if the engine has been successfully initialized, false if not
55: */
56: public function init($settings = array()) {
57: if (!class_exists('Redis')) {
58: return false;
59: }
60: parent::init(array_merge(array(
61: 'engine' => 'Redis',
62: 'prefix' => Inflector::slug(APP_DIR) . '_',
63: 'server' => '127.0.0.1',
64: 'database' => 0,
65: 'port' => 6379,
66: 'password' => false,
67: 'timeout' => 0,
68: 'persistent' => true,
69: 'unix_socket' => false
70: ), $settings)
71: );
72:
73: return $this->_connect();
74: }
75:
76: /**
77: * Connects to a Redis server
78: *
79: * @return bool True if Redis server was connected
80: */
81: protected function _connect() {
82: try {
83: $this->_Redis = new Redis();
84: if (!empty($this->settings['unix_socket'])) {
85: $return = $this->_Redis->connect($this->settings['unix_socket']);
86: } elseif (empty($this->settings['persistent'])) {
87: $return = $this->_Redis->connect($this->settings['server'], $this->settings['port'], $this->settings['timeout']);
88: } else {
89: $persistentId = $this->settings['port'] . $this->settings['timeout'] . $this->settings['database'];
90: $return = $this->_Redis->pconnect($this->settings['server'], $this->settings['port'], $this->settings['timeout'], $persistentId);
91: }
92: } catch (RedisException $e) {
93: $return = false;
94: }
95: if (!$return) {
96: return false;
97: }
98: if ($this->settings['password'] && !$this->_Redis->auth($this->settings['password'])) {
99: return false;
100: }
101: return $this->_Redis->select($this->settings['database']);
102: }
103:
104: /**
105: * Write data for key into cache.
106: *
107: * @param string $key Identifier for the data
108: * @param mixed $value Data to be cached
109: * @param int $duration How long to cache the data, in seconds
110: * @return bool True if the data was successfully cached, false on failure
111: */
112: public function write($key, $value, $duration) {
113: if (!is_int($value)) {
114: $value = serialize($value);
115: }
116:
117: if (!$this->_Redis->isConnected()) {
118: $this->_connect();
119: }
120:
121: if ($duration === 0) {
122: return $this->_Redis->set($key, $value);
123: }
124:
125: return $this->_Redis->setex($key, $duration, $value);
126: }
127:
128: /**
129: * Read a key from the cache
130: *
131: * @param string $key Identifier for the data
132: * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it
133: */
134: public function read($key) {
135: $value = $this->_Redis->get($key);
136: if (preg_match('/^[-]?\d+$/', $value)) {
137: return (int)$value;
138: }
139: if ($value !== false && is_string($value)) {
140: return unserialize($value);
141: }
142: return $value;
143: }
144:
145: /**
146: * Increments the value of an integer cached key
147: *
148: * @param string $key Identifier for the data
149: * @param int $offset How much to increment
150: * @return New incremented value, false otherwise
151: * @throws CacheException when you try to increment with compress = true
152: */
153: public function increment($key, $offset = 1) {
154: return (int)$this->_Redis->incrBy($key, $offset);
155: }
156:
157: /**
158: * Decrements the value of an integer cached key
159: *
160: * @param string $key Identifier for the data
161: * @param int $offset How much to subtract
162: * @return New decremented value, false otherwise
163: * @throws CacheException when you try to decrement with compress = true
164: */
165: public function decrement($key, $offset = 1) {
166: return (int)$this->_Redis->decrBy($key, $offset);
167: }
168:
169: /**
170: * Delete a key from the cache
171: *
172: * @param string $key Identifier for the data
173: * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed
174: */
175: public function delete($key) {
176: return $this->_Redis->delete($key) > 0;
177: }
178:
179: /**
180: * Delete all keys from the cache
181: *
182: * @param bool $check Whether or not expiration keys should be checked. If
183: * true, no keys will be removed as cache will rely on redis TTL's.
184: * @return bool True if the cache was successfully cleared, false otherwise
185: */
186: public function clear($check) {
187: if ($check) {
188: return true;
189: }
190: $keys = $this->_Redis->getKeys($this->settings['prefix'] . '*');
191: $this->_Redis->del($keys);
192:
193: return true;
194: }
195:
196: /**
197: * Returns the `group value` for each of the configured groups
198: * If the group initial value was not found, then it initializes
199: * the group accordingly.
200: *
201: * @return array
202: */
203: public function groups() {
204: $result = array();
205: foreach ($this->settings['groups'] as $group) {
206: $value = $this->_Redis->get($this->settings['prefix'] . $group);
207: if (!$value) {
208: $value = 1;
209: $this->_Redis->set($this->settings['prefix'] . $group, $value);
210: }
211: $result[] = $group . $value;
212: }
213: return $result;
214: }
215:
216: /**
217: * Increments the group value to simulate deletion of all keys under a group
218: * old values will remain in storage until they expire.
219: *
220: * @param string $group The group name to clear.
221: * @return bool success
222: */
223: public function clearGroup($group) {
224: return (bool)$this->_Redis->incr($this->settings['prefix'] . $group);
225: }
226:
227: /**
228: * Disconnects from the redis server
229: */
230: public function __destruct() {
231: if (empty($this->settings['persistent']) && $this->_Redis !== null) {
232: $this->_Redis->close();
233: }
234: }
235:
236: /**
237: * Write data for key into cache if it doesn't exist already.
238: * If it already exists, it fails and returns false.
239: *
240: * @param string $key Identifier for the data.
241: * @param mixed $value Data to be cached.
242: * @param int $duration How long to cache the data, in seconds.
243: * @return bool True if the data was successfully cached, false on failure.
244: * @link https://github.com/phpredis/phpredis#setnx
245: */
246: public function add($key, $value, $duration) {
247: if (!is_int($value)) {
248: $value = serialize($value);
249: }
250:
251: $result = $this->_Redis->setnx($key, $value);
252: // setnx() doesn't have an expiry option, so overwrite the key with one
253: if ($result) {
254: return $this->_Redis->setex($key, $duration, $value);
255: }
256: return false;
257: }
258: }
259: