1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Cache\Traits; 13 14use Symfony\Component\Cache\Exception\CacheException; 15use Symfony\Component\Cache\Exception\InvalidArgumentException; 16 17/** 18 * @author Rob Frawley 2nd <rmf@src.run> 19 * @author Nicolas Grekas <p@tchwork.com> 20 * 21 * @internal 22 */ 23trait MemcachedTrait 24{ 25 private static $defaultClientOptions = [ 26 'persistent_id' => null, 27 'username' => null, 28 'password' => null, 29 \Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP, 30 ]; 31 32 private $client; 33 private $lazyClient; 34 35 public static function isSupported() 36 { 37 return \extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>='); 38 } 39 40 private function init(\Memcached $client, $namespace, $defaultLifetime) 41 { 42 if (!static::isSupported()) { 43 throw new CacheException('Memcached >= 2.2.0 is required.'); 44 } 45 if ('Memcached' === \get_class($client)) { 46 $opt = $client->getOption(\Memcached::OPT_SERIALIZER); 47 if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) { 48 throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".'); 49 } 50 $this->maxIdLength -= \strlen($client->getOption(\Memcached::OPT_PREFIX_KEY)); 51 $this->client = $client; 52 } else { 53 $this->lazyClient = $client; 54 } 55 56 parent::__construct($namespace, $defaultLifetime); 57 $this->enableVersioning(); 58 } 59 60 /** 61 * Creates a Memcached instance. 62 * 63 * By default, the binary protocol, no block, and libketama compatible options are enabled. 64 * 65 * Examples for servers: 66 * - 'memcached://user:pass@localhost?weight=33' 67 * - [['localhost', 11211, 33]] 68 * 69 * @param array[]|string|string[] $servers An array of servers, a DSN, or an array of DSNs 70 * @param array $options An array of options 71 * 72 * @return \Memcached 73 * 74 * @throws \ErrorException When invalid options or servers are provided 75 */ 76 public static function createConnection($servers, array $options = []) 77 { 78 if (\is_string($servers)) { 79 $servers = [$servers]; 80 } elseif (!\is_array($servers)) { 81 throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, "%s" given.', \gettype($servers))); 82 } 83 if (!static::isSupported()) { 84 throw new CacheException('Memcached >= 2.2.0 is required.'); 85 } 86 set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); 87 try { 88 $options += static::$defaultClientOptions; 89 $client = new \Memcached($options['persistent_id']); 90 $username = $options['username']; 91 $password = $options['password']; 92 93 // parse any DSN in $servers 94 foreach ($servers as $i => $dsn) { 95 if (\is_array($dsn)) { 96 continue; 97 } 98 if (0 !== strpos($dsn, 'memcached://')) { 99 throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s" does not start with "memcached://".', $dsn)); 100 } 101 $params = preg_replace_callback('#^memcached://(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) { 102 if (!empty($m[1])) { 103 list($username, $password) = explode(':', $m[1], 2) + [1 => null]; 104 } 105 106 return 'file://'; 107 }, $dsn); 108 if (false === $params = parse_url($params)) { 109 throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".', $dsn)); 110 } 111 if (!isset($params['host']) && !isset($params['path'])) { 112 throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".', $dsn)); 113 } 114 if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) { 115 $params['weight'] = $m[1]; 116 $params['path'] = substr($params['path'], 0, -\strlen($m[0])); 117 } 118 $params += [ 119 'host' => isset($params['host']) ? $params['host'] : $params['path'], 120 'port' => isset($params['host']) ? 11211 : null, 121 'weight' => 0, 122 ]; 123 if (isset($params['query'])) { 124 parse_str($params['query'], $query); 125 $params += $query; 126 $options = $query + $options; 127 } 128 129 $servers[$i] = [$params['host'], $params['port'], $params['weight']]; 130 } 131 132 // set client's options 133 unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']); 134 $options = array_change_key_case($options, \CASE_UPPER); 135 $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); 136 $client->setOption(\Memcached::OPT_NO_BLOCK, true); 137 $client->setOption(\Memcached::OPT_TCP_NODELAY, true); 138 if (!\array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !\array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) { 139 $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true); 140 } 141 foreach ($options as $name => $value) { 142 if (\is_int($name)) { 143 continue; 144 } 145 if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) { 146 $value = \constant('Memcached::'.$name.'_'.strtoupper($value)); 147 } 148 $opt = \constant('Memcached::OPT_'.$name); 149 150 unset($options[$name]); 151 $options[$opt] = $value; 152 } 153 $client->setOptions($options); 154 155 // set client's servers, taking care of persistent connections 156 if (!$client->isPristine()) { 157 $oldServers = []; 158 foreach ($client->getServerList() as $server) { 159 $oldServers[] = [$server['host'], $server['port']]; 160 } 161 162 $newServers = []; 163 foreach ($servers as $server) { 164 if (1 < \count($server)) { 165 $server = array_values($server); 166 unset($server[2]); 167 $server[1] = (int) $server[1]; 168 } 169 $newServers[] = $server; 170 } 171 172 if ($oldServers !== $newServers) { 173 $client->resetServerList(); 174 $client->addServers($servers); 175 } 176 } else { 177 $client->addServers($servers); 178 } 179 180 if (null !== $username || null !== $password) { 181 if (!method_exists($client, 'setSaslAuthData')) { 182 trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.'); 183 } 184 $client->setSaslAuthData($username, $password); 185 } 186 187 return $client; 188 } finally { 189 restore_error_handler(); 190 } 191 } 192 193 /** 194 * {@inheritdoc} 195 */ 196 protected function doSave(array $values, $lifetime) 197 { 198 if ($lifetime && $lifetime > 30 * 86400) { 199 $lifetime += time(); 200 } 201 202 $encodedValues = []; 203 foreach ($values as $key => $value) { 204 $encodedValues[rawurlencode($key)] = $value; 205 } 206 207 return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime)); 208 } 209 210 /** 211 * {@inheritdoc} 212 */ 213 protected function doFetch(array $ids) 214 { 215 $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); 216 try { 217 $encodedIds = array_map('rawurlencode', $ids); 218 219 $encodedResult = $this->checkResultCode($this->getClient()->getMulti($encodedIds)); 220 221 $result = []; 222 foreach ($encodedResult as $key => $value) { 223 $result[rawurldecode($key)] = $value; 224 } 225 226 return $result; 227 } catch (\Error $e) { 228 throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); 229 } finally { 230 ini_set('unserialize_callback_func', $unserializeCallbackHandler); 231 } 232 } 233 234 /** 235 * {@inheritdoc} 236 */ 237 protected function doHave($id) 238 { 239 return false !== $this->getClient()->get(rawurlencode($id)) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode()); 240 } 241 242 /** 243 * {@inheritdoc} 244 */ 245 protected function doDelete(array $ids) 246 { 247 $ok = true; 248 $encodedIds = array_map('rawurlencode', $ids); 249 foreach ($this->checkResultCode($this->getClient()->deleteMulti($encodedIds)) as $result) { 250 if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) { 251 $ok = false; 252 break; 253 } 254 } 255 256 return $ok; 257 } 258 259 /** 260 * {@inheritdoc} 261 */ 262 protected function doClear($namespace) 263 { 264 return '' === $namespace && $this->getClient()->flush(); 265 } 266 267 private function checkResultCode($result) 268 { 269 $code = $this->client->getResultCode(); 270 271 if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) { 272 return $result; 273 } 274 275 throw new CacheException('MemcachedAdapter client error: '.strtolower($this->client->getResultMessage())); 276 } 277 278 /** 279 * @return \Memcached 280 */ 281 private function getClient() 282 { 283 if ($this->client) { 284 return $this->client; 285 } 286 287 $opt = $this->lazyClient->getOption(\Memcached::OPT_SERIALIZER); 288 if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) { 289 throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".'); 290 } 291 if ('' !== $prefix = (string) $this->lazyClient->getOption(\Memcached::OPT_PREFIX_KEY)) { 292 throw new CacheException(sprintf('MemcachedAdapter: "prefix_key" option must be empty when using proxified connections, "%s" given.', $prefix)); 293 } 294 295 return $this->client = $this->lazyClient; 296 } 297} 298