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.5.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Cache\Engine;
16:
17: use Cake\Cache\CacheEngine;
18: use InvalidArgumentException;
19: use Memcached;
20:
21: /**
22: * Memcached storage engine for cache. Memcached has some limitations in the amount of
23: * control you have over expire times far in the future. See MemcachedEngine::write() for
24: * more information.
25: *
26: * Memcached engine supports binary protocol and igbinary
27: * serialization (if memcached extension is compiled with --enable-igbinary).
28: * Compressed keys can also be incremented/decremented.
29: */
30: class MemcachedEngine extends CacheEngine
31: {
32: /**
33: * memcached wrapper.
34: *
35: * @var \Memcached
36: */
37: protected $_Memcached;
38:
39: /**
40: * The default config used unless overridden by runtime configuration
41: *
42: * - `compress` Whether to compress data
43: * - `duration` Specify how long items in this cache configuration last.
44: * - `groups` List of groups or 'tags' associated to every key stored in this config.
45: * handy for deleting a complete group from cache.
46: * - `username` Login to access the Memcache server
47: * - `password` Password to access the Memcache server
48: * - `persistent` The name of the persistent connection. All configurations using
49: * the same persistent value will share a single underlying connection.
50: * - `prefix` Prepended to all entries. Good for when you need to share a keyspace
51: * with either another cache config or another application.
52: * - `probability` Probability of hitting a cache gc cleanup. Setting to 0 will disable
53: * cache::gc from ever being called automatically.
54: * - `serialize` The serializer engine used to serialize data. Available engines are php,
55: * igbinary and json. Beside php, the memcached extension must be compiled with the
56: * appropriate serializer support.
57: * - `servers` String or array of memcached servers. If an array MemcacheEngine will use
58: * them as a pool.
59: * - `options` - Additional options for the memcached client. Should be an array of option => value.
60: * Use the \Memcached::OPT_* constants as keys.
61: *
62: * @var array
63: */
64: protected $_defaultConfig = [
65: 'compress' => false,
66: 'duration' => 3600,
67: 'groups' => [],
68: 'host' => null,
69: 'username' => null,
70: 'password' => null,
71: 'persistent' => false,
72: 'port' => null,
73: 'prefix' => 'cake_',
74: 'probability' => 100,
75: 'serialize' => 'php',
76: 'servers' => ['127.0.0.1'],
77: 'options' => [],
78: ];
79:
80: /**
81: * List of available serializer engines
82: *
83: * Memcached must be compiled with json and igbinary support to use these engines
84: *
85: * @var array
86: */
87: protected $_serializers = [];
88:
89: /**
90: * @var string[]
91: */
92: protected $_compiledGroupNames = [];
93:
94: /**
95: * Initialize the Cache Engine
96: *
97: * Called automatically by the cache frontend
98: *
99: * @param array $config array of setting for the engine
100: * @return bool True if the engine has been successfully initialized, false if not
101: * @throws \InvalidArgumentException When you try use authentication without
102: * Memcached compiled with SASL support
103: */
104: public function init(array $config = [])
105: {
106: if (!extension_loaded('memcached')) {
107: return false;
108: }
109:
110: $this->_serializers = [
111: 'igbinary' => Memcached::SERIALIZER_IGBINARY,
112: 'json' => Memcached::SERIALIZER_JSON,
113: 'php' => Memcached::SERIALIZER_PHP
114: ];
115: if (defined('Memcached::HAVE_MSGPACK') && Memcached::HAVE_MSGPACK) {
116: $this->_serializers['msgpack'] = Memcached::SERIALIZER_MSGPACK;
117: }
118:
119: parent::init($config);
120:
121: if (!empty($config['host'])) {
122: if (empty($config['port'])) {
123: $config['servers'] = [$config['host']];
124: } else {
125: $config['servers'] = [sprintf('%s:%d', $config['host'], $config['port'])];
126: }
127: }
128:
129: if (isset($config['servers'])) {
130: $this->setConfig('servers', $config['servers'], false);
131: }
132:
133: if (!is_array($this->_config['servers'])) {
134: $this->_config['servers'] = [$this->_config['servers']];
135: }
136:
137: if (isset($this->_Memcached)) {
138: return true;
139: }
140:
141: if ($this->_config['persistent']) {
142: $this->_Memcached = new Memcached((string)$this->_config['persistent']);
143: } else {
144: $this->_Memcached = new Memcached();
145: }
146: $this->_setOptions();
147:
148: if (count($this->_Memcached->getServerList())) {
149: return true;
150: }
151:
152: $servers = [];
153: foreach ($this->_config['servers'] as $server) {
154: $servers[] = $this->parseServerString($server);
155: }
156:
157: if (!$this->_Memcached->addServers($servers)) {
158: return false;
159: }
160:
161: if (is_array($this->_config['options'])) {
162: foreach ($this->_config['options'] as $opt => $value) {
163: $this->_Memcached->setOption($opt, $value);
164: }
165: }
166:
167: if (empty($this->_config['username']) && !empty($this->_config['login'])) {
168: throw new InvalidArgumentException(
169: 'Please pass "username" instead of "login" for connecting to Memcached'
170: );
171: }
172:
173: if ($this->_config['username'] !== null && $this->_config['password'] !== null) {
174: if (!method_exists($this->_Memcached, 'setSaslAuthData')) {
175: throw new InvalidArgumentException(
176: 'Memcached extension is not built with SASL support'
177: );
178: }
179: $this->_Memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
180: $this->_Memcached->setSaslAuthData(
181: $this->_config['username'],
182: $this->_config['password']
183: );
184: }
185:
186: return true;
187: }
188:
189: /**
190: * Settings the memcached instance
191: *
192: * @return void
193: * @throws \InvalidArgumentException When the Memcached extension is not built
194: * with the desired serializer engine.
195: */
196: protected function _setOptions()
197: {
198: $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
199:
200: $serializer = strtolower($this->_config['serialize']);
201: if (!isset($this->_serializers[$serializer])) {
202: throw new InvalidArgumentException(
203: sprintf('%s is not a valid serializer engine for Memcached', $serializer)
204: );
205: }
206:
207: if ($serializer !== 'php' &&
208: !constant('Memcached::HAVE_' . strtoupper($serializer))
209: ) {
210: throw new InvalidArgumentException(
211: sprintf('Memcached extension is not compiled with %s support', $serializer)
212: );
213: }
214:
215: $this->_Memcached->setOption(
216: Memcached::OPT_SERIALIZER,
217: $this->_serializers[$serializer]
218: );
219:
220: // Check for Amazon ElastiCache instance
221: if (defined('Memcached::OPT_CLIENT_MODE') &&
222: defined('Memcached::DYNAMIC_CLIENT_MODE')
223: ) {
224: $this->_Memcached->setOption(
225: Memcached::OPT_CLIENT_MODE,
226: Memcached::DYNAMIC_CLIENT_MODE
227: );
228: }
229:
230: $this->_Memcached->setOption(
231: Memcached::OPT_COMPRESSION,
232: (bool)$this->_config['compress']
233: );
234: }
235:
236: /**
237: * Parses the server address into the host/port. Handles both IPv6 and IPv4
238: * addresses and Unix sockets
239: *
240: * @param string $server The server address string.
241: * @return array Array containing host, port
242: */
243: public function parseServerString($server)
244: {
245: $socketTransport = 'unix://';
246: if (strpos($server, $socketTransport) === 0) {
247: return [substr($server, strlen($socketTransport)), 0];
248: }
249: if (substr($server, 0, 1) === '[') {
250: $position = strpos($server, ']:');
251: if ($position !== false) {
252: $position++;
253: }
254: } else {
255: $position = strpos($server, ':');
256: }
257: $port = 11211;
258: $host = $server;
259: if ($position !== false) {
260: $host = substr($server, 0, $position);
261: $port = substr($server, $position + 1);
262: }
263:
264: return [$host, (int)$port];
265: }
266:
267: /**
268: * Backwards compatible alias of parseServerString
269: *
270: * @param string $server The server address string.
271: * @return array Array containing host, port
272: * @deprecated 3.4.13 Will be removed in 4.0.0
273: */
274: protected function _parseServerString($server)
275: {
276: return $this->parseServerString($server);
277: }
278:
279: /**
280: * Read an option value from the memcached connection.
281: *
282: * @param string $name The option name to read.
283: * @return string|int|bool|null
284: */
285: public function getOption($name)
286: {
287: return $this->_Memcached->getOption($name);
288: }
289:
290: /**
291: * Write data for key into cache. When using memcached as your cache engine
292: * remember that the Memcached pecl extension does not support cache expiry
293: * times greater than 30 days in the future. Any duration greater than 30 days
294: * will be treated as never expiring.
295: *
296: * @param string $key Identifier for the data
297: * @param mixed $value Data to be cached
298: * @return bool True if the data was successfully cached, false on failure
299: * @see https://secure.php.net/manual/en/memcache.set.php
300: */
301: public function write($key, $value)
302: {
303: $duration = $this->_config['duration'];
304: if ($duration > 30 * DAY) {
305: $duration = 0;
306: }
307:
308: $key = $this->_key($key);
309:
310: return $this->_Memcached->set($key, $value, $duration);
311: }
312:
313: /**
314: * Write many cache entries to the cache at once
315: *
316: * @param array $data An array of data to be stored in the cache
317: * @return array of bools for each key provided, true if the data was
318: * successfully cached, false on failure
319: */
320: public function writeMany($data)
321: {
322: $cacheData = [];
323: foreach ($data as $key => $value) {
324: $cacheData[$this->_key($key)] = $value;
325: }
326:
327: $success = $this->_Memcached->setMulti($cacheData);
328:
329: $return = [];
330: foreach (array_keys($data) as $key) {
331: $return[$key] = $success;
332: }
333:
334: return $return;
335: }
336:
337: /**
338: * Read a key from the cache
339: *
340: * @param string $key Identifier for the data
341: * @return mixed The cached data, or false if the data doesn't exist, has
342: * expired, or if there was an error fetching it.
343: */
344: public function read($key)
345: {
346: $key = $this->_key($key);
347:
348: return $this->_Memcached->get($key);
349: }
350:
351: /**
352: * Read many keys from the cache at once
353: *
354: * @param array $keys An array of identifiers for the data
355: * @return array An array containing, for each of the given $keys, the cached data or
356: * false if cached data could not be retrieved.
357: */
358: public function readMany($keys)
359: {
360: $cacheKeys = [];
361: foreach ($keys as $key) {
362: $cacheKeys[] = $this->_key($key);
363: }
364:
365: $values = $this->_Memcached->getMulti($cacheKeys);
366: $return = [];
367: foreach ($keys as &$key) {
368: $return[$key] = array_key_exists($this->_key($key), $values) ?
369: $values[$this->_key($key)] : false;
370: }
371:
372: return $return;
373: }
374:
375: /**
376: * Increments the value of an integer cached key
377: *
378: * @param string $key Identifier for the data
379: * @param int $offset How much to increment
380: * @return int|false New incremented value, false otherwise
381: */
382: public function increment($key, $offset = 1)
383: {
384: $key = $this->_key($key);
385:
386: return $this->_Memcached->increment($key, $offset);
387: }
388:
389: /**
390: * Decrements the value of an integer cached key
391: *
392: * @param string $key Identifier for the data
393: * @param int $offset How much to subtract
394: * @return int|false New decremented value, false otherwise
395: */
396: public function decrement($key, $offset = 1)
397: {
398: $key = $this->_key($key);
399:
400: return $this->_Memcached->decrement($key, $offset);
401: }
402:
403: /**
404: * Delete a key from the cache
405: *
406: * @param string $key Identifier for the data
407: * @return bool True if the value was successfully deleted, false if it didn't
408: * exist or couldn't be removed.
409: */
410: public function delete($key)
411: {
412: $key = $this->_key($key);
413:
414: return $this->_Memcached->delete($key);
415: }
416:
417: /**
418: * Delete many keys from the cache at once
419: *
420: * @param array $keys An array of identifiers for the data
421: * @return array of boolean values that are true if the key was successfully
422: * deleted, false if it didn't exist or couldn't be removed.
423: */
424: public function deleteMany($keys)
425: {
426: $cacheKeys = [];
427: foreach ($keys as $key) {
428: $cacheKeys[] = $this->_key($key);
429: }
430:
431: $success = $this->_Memcached->deleteMulti($cacheKeys);
432:
433: $return = [];
434: foreach ($keys as $key) {
435: $return[$key] = $success;
436: }
437:
438: return $return;
439: }
440:
441: /**
442: * Delete all keys from the cache
443: *
444: * @param bool $check If true will check expiration, otherwise delete all.
445: * @return bool True if the cache was successfully cleared, false otherwise
446: */
447: public function clear($check)
448: {
449: if ($check) {
450: return true;
451: }
452:
453: $keys = $this->_Memcached->getAllKeys();
454: if ($keys === false) {
455: return false;
456: }
457:
458: foreach ($keys as $key) {
459: if (strpos($key, $this->_config['prefix']) === 0) {
460: $this->_Memcached->delete($key);
461: }
462: }
463:
464: return true;
465: }
466:
467: /**
468: * Add a key to the cache if it does not already exist.
469: *
470: * @param string $key Identifier for the data.
471: * @param mixed $value Data to be cached.
472: * @return bool True if the data was successfully cached, false on failure.
473: */
474: public function add($key, $value)
475: {
476: $duration = $this->_config['duration'];
477: if ($duration > 30 * DAY) {
478: $duration = 0;
479: }
480:
481: $key = $this->_key($key);
482:
483: return $this->_Memcached->add($key, $value, $duration);
484: }
485:
486: /**
487: * Returns the `group value` for each of the configured groups
488: * If the group initial value was not found, then it initializes
489: * the group accordingly.
490: *
491: * @return array
492: */
493: public function groups()
494: {
495: if (empty($this->_compiledGroupNames)) {
496: foreach ($this->_config['groups'] as $group) {
497: $this->_compiledGroupNames[] = $this->_config['prefix'] . $group;
498: }
499: }
500:
501: $groups = $this->_Memcached->getMulti($this->_compiledGroupNames) ?: [];
502: if (count($groups) !== count($this->_config['groups'])) {
503: foreach ($this->_compiledGroupNames as $group) {
504: if (!isset($groups[$group])) {
505: $this->_Memcached->set($group, 1, 0);
506: $groups[$group] = 1;
507: }
508: }
509: ksort($groups);
510: }
511:
512: $result = [];
513: $groups = array_values($groups);
514: foreach ($this->_config['groups'] as $i => $group) {
515: $result[] = $group . $groups[$i];
516: }
517:
518: return $result;
519: }
520:
521: /**
522: * Increments the group value to simulate deletion of all keys under a group
523: * old values will remain in storage until they expire.
524: *
525: * @param string $group name of the group to be cleared
526: * @return bool success
527: */
528: public function clearGroup($group)
529: {
530: return (bool)$this->_Memcached->increment($this->_config['prefix'] . $group);
531: }
532: }
533: