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