1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Cache\Storage\Adapter; 11 12use Memcache as MemcacheResource; 13use stdClass; 14use Traversable; 15use Zend\Cache\Exception; 16use Zend\Cache\Storage\AvailableSpaceCapableInterface; 17use Zend\Cache\Storage\Capabilities; 18use Zend\Cache\Storage\FlushableInterface; 19use Zend\Cache\Storage\TotalSpaceCapableInterface; 20 21class Memcache extends AbstractAdapter implements 22 AvailableSpaceCapableInterface, 23 FlushableInterface, 24 TotalSpaceCapableInterface 25{ 26 /** 27 * Has this instance been initialized 28 * 29 * @var bool 30 */ 31 protected $initialized = false; 32 33 /** 34 * The memcache resource manager 35 * 36 * @var null|MemcacheResourceManager 37 */ 38 protected $resourceManager; 39 40 /** 41 * The memcache resource id 42 * 43 * @var null|string 44 */ 45 protected $resourceId; 46 47 /** 48 * The namespace prefix 49 * 50 * @var string 51 */ 52 protected $namespacePrefix = ''; 53 54 /** 55 * Constructor 56 * 57 * @param null|array|Traversable|MemcacheOptions $options 58 * @throws Exception\ExceptionInterface 59 */ 60 public function __construct($options = null) 61 { 62 if (version_compare('2.0.0', phpversion('memcache')) > 0) { 63 throw new Exception\ExtensionNotLoadedException("Missing ext/memcache version >= 2.0.0"); 64 } 65 66 parent::__construct($options); 67 68 // reset initialized flag on update option(s) 69 $initialized = & $this->initialized; 70 $this->getEventManager()->attach('option', function () use (& $initialized) { 71 $initialized = false; 72 }); 73 } 74 75 /** 76 * Initialize the internal memcache resource 77 * 78 * @return MemcacheResource 79 */ 80 protected function getMemcacheResource() 81 { 82 if ($this->initialized) { 83 return $this->resourceManager->getResource($this->resourceId); 84 } 85 86 $options = $this->getOptions(); 87 88 // get resource manager and resource id 89 $this->resourceManager = $options->getResourceManager(); 90 $this->resourceId = $options->getResourceId(); 91 92 // init namespace prefix 93 $this->namespacePrefix = ''; 94 $namespace = $options->getNamespace(); 95 if ($namespace !== '') { 96 $this->namespacePrefix = $namespace . $options->getNamespaceSeparator(); 97 } 98 99 // update initialized flag 100 $this->initialized = true; 101 102 return $this->resourceManager->getResource($this->resourceId); 103 } 104 105 /* options */ 106 107 /** 108 * Set options. 109 * 110 * @param array|Traversable|MemcacheOptions $options 111 * @return Memcache 112 * @see getOptions() 113 */ 114 public function setOptions($options) 115 { 116 if (!$options instanceof MemcacheOptions) { 117 $options = new MemcacheOptions($options); 118 } 119 120 return parent::setOptions($options); 121 } 122 123 /** 124 * Get options. 125 * 126 * @return MemcacheOptions 127 * @see setOptions() 128 */ 129 public function getOptions() 130 { 131 if (!$this->options) { 132 $this->setOptions(new MemcacheOptions()); 133 } 134 return $this->options; 135 } 136 137 /** 138 * @param mixed $value 139 * @return int 140 */ 141 protected function getWriteFlag(& $value) 142 { 143 if (!$this->getOptions()->getCompression()) { 144 return 0; 145 } 146 // Don't compress numeric or boolean types 147 return (is_bool($value) || is_int($value) || is_float($value)) ? 0 : MEMCACHE_COMPRESSED; 148 } 149 150 /* FlushableInterface */ 151 152 /** 153 * Flush the whole storage 154 * 155 * @return bool 156 */ 157 public function flush() 158 { 159 $memc = $this->getMemcacheResource(); 160 if (!$memc->flush()) { 161 return new Exception\RuntimeException("Memcache flush failed"); 162 } 163 return true; 164 } 165 166 /* TotalSpaceCapableInterface */ 167 168 /** 169 * Get total space in bytes 170 * 171 * @return int|float 172 */ 173 public function getTotalSpace() 174 { 175 $memc = $this->getMemcacheResource(); 176 $stats = $memc->getExtendedStats(); 177 if ($stats === false) { 178 return new Exception\RuntimeException("Memcache getStats failed"); 179 } 180 181 $mem = array_pop($stats); 182 return $mem['limit_maxbytes']; 183 } 184 185 /* AvailableSpaceCapableInterface */ 186 187 /** 188 * Get available space in bytes 189 * 190 * @return int|float 191 */ 192 public function getAvailableSpace() 193 { 194 $memc = $this->getMemcacheResource(); 195 $stats = $memc->getExtendedStats(); 196 if ($stats === false) { 197 throw new Exception\RuntimeException('Memcache getStats failed'); 198 } 199 200 $mem = array_pop($stats); 201 return $mem['limit_maxbytes'] - $mem['bytes']; 202 } 203 204 /* reading */ 205 206 /** 207 * Internal method to get an item. 208 * 209 * @param string $normalizedKey 210 * @param bool $success 211 * @param mixed $casToken 212 * @return mixed Data on success, null on failure 213 * @throws Exception\ExceptionInterface 214 */ 215 protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) 216 { 217 $memc = $this->getMemcacheResource(); 218 $internalKey = $this->namespacePrefix . $normalizedKey; 219 220 $result = $memc->get($internalKey); 221 $success = ($result !== false); 222 if ($result === false) { 223 return; 224 } 225 226 $casToken = $result; 227 return $result; 228 } 229 230 /** 231 * Internal method to get multiple items. 232 * 233 * @param array $normalizedKeys 234 * @return array Associative array of keys and values 235 * @throws Exception\ExceptionInterface 236 */ 237 protected function internalGetItems(array & $normalizedKeys) 238 { 239 $memc = $this->getMemcacheResource(); 240 241 foreach ($normalizedKeys as & $normalizedKey) { 242 $normalizedKey = $this->namespacePrefix . $normalizedKey; 243 } 244 245 $result = $memc->get($normalizedKeys); 246 if ($result === false) { 247 return array(); 248 } 249 250 // remove namespace prefix from result 251 if ($this->namespacePrefix !== '') { 252 $tmp = array(); 253 $nsPrefixLength = strlen($this->namespacePrefix); 254 foreach ($result as $internalKey => & $value) { 255 $tmp[substr($internalKey, $nsPrefixLength)] = & $value; 256 } 257 $result = $tmp; 258 } 259 260 return $result; 261 } 262 263 /** 264 * Internal method to test if an item exists. 265 * 266 * @param string $normalizedKey 267 * @return bool 268 * @throws Exception\ExceptionInterface 269 */ 270 protected function internalHasItem(& $normalizedKey) 271 { 272 $memc = $this->getMemcacheResource(); 273 $value = $memc->get($this->namespacePrefix . $normalizedKey); 274 return ($value !== false); 275 } 276 277 /** 278 * Internal method to test multiple items. 279 * 280 * @param array $normalizedKeys 281 * @return array Array of found keys 282 * @throws Exception\ExceptionInterface 283 */ 284 protected function internalHasItems(array & $normalizedKeys) 285 { 286 $memc = $this->getMemcacheResource(); 287 288 foreach ($normalizedKeys as & $normalizedKey) { 289 $normalizedKey = $this->namespacePrefix . $normalizedKey; 290 } 291 292 $result = $memc->get($normalizedKeys); 293 if ($result === false) { 294 return array(); 295 } 296 297 // Convert to a single list 298 $result = array_keys($result); 299 300 // remove namespace prefix 301 if ($result && $this->namespacePrefix !== '') { 302 $nsPrefixLength = strlen($this->namespacePrefix); 303 foreach ($result as & $internalKey) { 304 $internalKey = substr($internalKey, $nsPrefixLength); 305 } 306 } 307 308 return $result; 309 } 310 311 /** 312 * Get metadata of multiple items 313 * 314 * @param array $normalizedKeys 315 * @return array Associative array of keys and metadata 316 * @throws Exception\ExceptionInterface 317 */ 318 protected function internalGetMetadatas(array & $normalizedKeys) 319 { 320 $memc = $this->getMemcacheResource(); 321 322 foreach ($normalizedKeys as & $normalizedKey) { 323 $normalizedKey = $this->namespacePrefix . $normalizedKey; 324 } 325 326 $result = $memc->get($normalizedKeys); 327 if ($result === false) { 328 return array(); 329 } 330 331 // remove namespace prefix and use an empty array as metadata 332 if ($this->namespacePrefix === '') { 333 foreach ($result as & $value) { 334 $value = array(); 335 } 336 return $result; 337 } 338 339 $final = array(); 340 $nsPrefixLength = strlen($this->namespacePrefix); 341 foreach (array_keys($result) as $internalKey) { 342 $final[substr($internalKey, $nsPrefixLength)] = array(); 343 } 344 return $final; 345 } 346 347 /* writing */ 348 349 /** 350 * Internal method to store an item. 351 * 352 * @param string $normalizedKey 353 * @param mixed $value 354 * @return bool 355 * @throws Exception\ExceptionInterface 356 */ 357 protected function internalSetItem(& $normalizedKey, & $value) 358 { 359 $memc = $this->getMemcacheResource(); 360 $expiration = $this->expirationTime(); 361 $flag = $this->getWriteFlag($value); 362 363 if (!$memc->set($this->namespacePrefix . $normalizedKey, $value, $flag, $expiration)) { 364 throw new Exception\RuntimeException('Memcache set value failed'); 365 } 366 367 return true; 368 } 369 370 /** 371 * Add an item. 372 * 373 * @param string $normalizedKey 374 * @param mixed $value 375 * @return bool 376 * @throws Exception\ExceptionInterface 377 */ 378 protected function internalAddItem(& $normalizedKey, & $value) 379 { 380 $memc = $this->getMemcacheResource(); 381 $expiration = $this->expirationTime(); 382 $flag = $this->getWriteFlag($value); 383 384 return $memc->add($this->namespacePrefix . $normalizedKey, $value, $flag, $expiration); 385 } 386 387 /** 388 * Internal method to replace an existing item. 389 * 390 * @param string $normalizedKey 391 * @param mixed $value 392 * @return bool 393 * @throws Exception\ExceptionInterface 394 */ 395 protected function internalReplaceItem(& $normalizedKey, & $value) 396 { 397 $memc = $this->getMemcacheResource(); 398 $expiration = $this->expirationTime(); 399 $flag = $this->getWriteFlag($value); 400 401 return $memc->replace($this->namespacePrefix . $normalizedKey, $value, $flag, $expiration); 402 } 403 404 /** 405 * Internal method to remove an item. 406 * 407 * @param string $normalizedKey 408 * @return bool 409 * @throws Exception\ExceptionInterface 410 */ 411 protected function internalRemoveItem(& $normalizedKey) 412 { 413 $memc = $this->getMemcacheResource(); 414 // Delete's second parameter (timeout) is deprecated and not supported. 415 // Values other than 0 may cause delete to fail. 416 // http://www.php.net/manual/memcache.delete.php 417 return $memc->delete($this->namespacePrefix . $normalizedKey, 0); 418 } 419 420 /** 421 * Internal method to increment an item. 422 * 423 * @param string $normalizedKey 424 * @param int $value 425 * @return int|bool The new value on success, false on failure 426 * @throws Exception\ExceptionInterface 427 */ 428 protected function internalIncrementItem(& $normalizedKey, & $value) 429 { 430 $memc = $this->getMemcacheResource(); 431 $internalKey = $this->namespacePrefix . $normalizedKey; 432 $value = (int) $value; 433 $newValue = $memc->increment($internalKey, $value); 434 435 if ($newValue !== false) { 436 return $newValue; 437 } 438 439 // Set initial value. Don't use compression! 440 // http://www.php.net/manual/memcache.increment.php 441 $newValue = $value; 442 if (!$memc->add($internalKey, $newValue, 0, $this->expirationTime())) { 443 throw new Exception\RuntimeException('Memcache unable to add increment value'); 444 } 445 446 return $newValue; 447 } 448 449 /** 450 * Internal method to decrement an item. 451 * 452 * @param string $normalizedKey 453 * @param int $value 454 * @return int|bool The new value on success, false on failure 455 * @throws Exception\ExceptionInterface 456 */ 457 protected function internalDecrementItem(& $normalizedKey, & $value) 458 { 459 $memc = $this->getMemcacheResource(); 460 $internalKey = $this->namespacePrefix . $normalizedKey; 461 $value = (int) $value; 462 $newValue = $memc->decrement($internalKey, $value); 463 464 if ($newValue !== false) { 465 return $newValue; 466 } 467 468 // Set initial value. Don't use compression! 469 // http://www.php.net/manual/memcache.decrement.php 470 $newValue = -$value; 471 if (!$memc->add($internalKey, $newValue, 0, $this->expirationTime())) { 472 throw new Exception\RuntimeException('Memcache unable to add decrement value'); 473 } 474 475 return $newValue; 476 } 477 478 /* status */ 479 480 /** 481 * Internal method to get capabilities of this adapter 482 * 483 * @return Capabilities 484 */ 485 protected function internalGetCapabilities() 486 { 487 if ($this->capabilities !== null) { 488 return $this->capabilities; 489 } 490 491 if (version_compare('3.0.3', phpversion('memcache')) <= 0) { 492 // In ext/memcache v3.0.3: 493 // Scalar data types (int, bool, double) are preserved by get/set. 494 // http://pecl.php.net/package/memcache/3.0.3 495 // 496 // This effectively removes support for `boolean` types since 497 // "not found" return values are === false. 498 $supportedDatatypes = array( 499 'NULL' => true, 500 'boolean' => false, 501 'integer' => true, 502 'double' => true, 503 'string' => true, 504 'array' => true, 505 'object' => 'object', 506 'resource' => false, 507 ); 508 } else { 509 // In stable 2.x ext/memcache versions, scalar data types are 510 // converted to strings and must be manually cast back to original 511 // types by the user. 512 // 513 // ie. It is impossible to know if the saved value: (string)"1" 514 // was previously: (bool)true, (int)1, or (string)"1". 515 // Similarly, the saved value: (string)"" 516 // might have previously been: (bool)false or (string)"" 517 $supportedDatatypes = array( 518 'NULL' => true, 519 'boolean' => 'boolean', 520 'integer' => 'integer', 521 'double' => 'double', 522 'string' => true, 523 'array' => true, 524 'object' => 'object', 525 'resource' => false, 526 ); 527 } 528 529 $this->capabilityMarker = new stdClass(); 530 $this->capabilities = new Capabilities( 531 $this, 532 $this->capabilityMarker, 533 array( 534 'supportedDatatypes' => $supportedDatatypes, 535 'supportedMetadata' => array(), 536 'minTtl' => 1, 537 'maxTtl' => 0, 538 'staticTtl' => true, 539 'ttlPrecision' => 1, 540 'useRequestTime' => false, 541 'expiredRead' => false, 542 'maxKeyLength' => 255, 543 'namespaceIsPrefix' => true, 544 ) 545 ); 546 547 return $this->capabilities; 548 } 549 550 /* internal */ 551 552 /** 553 * Get expiration time by ttl 554 * 555 * Some storage commands involve sending an expiration value (relative to 556 * an item or to an operation requested by the client) to the server. In 557 * all such cases, the actual value sent may either be Unix time (number of 558 * seconds since January 1, 1970, as an integer), or a number of seconds 559 * starting from current time. In the latter case, this number of seconds 560 * may not exceed 60*60*24*30 (number of seconds in 30 days); if the 561 * expiration value is larger than that, the server will consider it to be 562 * real Unix time value rather than an offset from current time. 563 * 564 * @return int 565 */ 566 protected function expirationTime() 567 { 568 $ttl = $this->getOptions()->getTtl(); 569 if ($ttl > 2592000) { 570 return time() + $ttl; 571 } 572 return $ttl; 573 } 574} 575