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 Memcached as MemcachedResource; 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 Memcached extends AbstractAdapter implements 22 AvailableSpaceCapableInterface, 23 FlushableInterface, 24 TotalSpaceCapableInterface 25{ 26 /** 27 * Major version of ext/memcached 28 * 29 * @var null|int 30 */ 31 protected static $extMemcachedMajorVersion; 32 33 /** 34 * Has this instance be initialized 35 * 36 * @var bool 37 */ 38 protected $initialized = false; 39 40 /** 41 * The memcached resource manager 42 * 43 * @var null|MemcachedResourceManager 44 */ 45 protected $resourceManager; 46 47 /** 48 * The memcached resource id 49 * 50 * @var null|string 51 */ 52 protected $resourceId; 53 54 /** 55 * The namespace prefix 56 * 57 * @var string 58 */ 59 protected $namespacePrefix = ''; 60 61 /** 62 * Constructor 63 * 64 * @param null|array|Traversable|MemcachedOptions $options 65 * @throws Exception\ExceptionInterface 66 */ 67 public function __construct($options = null) 68 { 69 if (static::$extMemcachedMajorVersion === null) { 70 $v = (string) phpversion('memcached'); 71 static::$extMemcachedMajorVersion = ($v !== '') ? (int) $v[0] : 0; 72 } 73 74 if (static::$extMemcachedMajorVersion < 1) { 75 throw new Exception\ExtensionNotLoadedException('Need ext/memcached version >= 1.0.0'); 76 } 77 78 parent::__construct($options); 79 80 // reset initialized flag on update option(s) 81 $initialized = & $this->initialized; 82 $this->getEventManager()->attach('option', function () use (& $initialized) { 83 $initialized = false; 84 }); 85 } 86 87 /** 88 * Initialize the internal memcached resource 89 * 90 * @return MemcachedResource 91 */ 92 protected function getMemcachedResource() 93 { 94 if (!$this->initialized) { 95 $options = $this->getOptions(); 96 97 // get resource manager and resource id 98 $this->resourceManager = $options->getResourceManager(); 99 $this->resourceId = $options->getResourceId(); 100 101 // init namespace prefix 102 $namespace = $options->getNamespace(); 103 if ($namespace !== '') { 104 $this->namespacePrefix = $namespace . $options->getNamespaceSeparator(); 105 } else { 106 $this->namespacePrefix = ''; 107 } 108 109 // update initialized flag 110 $this->initialized = true; 111 } 112 113 return $this->resourceManager->getResource($this->resourceId); 114 } 115 116 /* options */ 117 118 /** 119 * Set options. 120 * 121 * @param array|Traversable|MemcachedOptions $options 122 * @return Memcached 123 * @see getOptions() 124 */ 125 public function setOptions($options) 126 { 127 if (!$options instanceof MemcachedOptions) { 128 $options = new MemcachedOptions($options); 129 } 130 131 return parent::setOptions($options); 132 } 133 134 /** 135 * Get options. 136 * 137 * @return MemcachedOptions 138 * @see setOptions() 139 */ 140 public function getOptions() 141 { 142 if (!$this->options) { 143 $this->setOptions(new MemcachedOptions()); 144 } 145 return $this->options; 146 } 147 148 /* FlushableInterface */ 149 150 /** 151 * Flush the whole storage 152 * 153 * @return bool 154 */ 155 public function flush() 156 { 157 $memc = $this->getMemcachedResource(); 158 if (!$memc->flush()) { 159 throw $this->getExceptionByResultCode($memc->getResultCode()); 160 } 161 return true; 162 } 163 164 /* TotalSpaceCapableInterface */ 165 166 /** 167 * Get total space in bytes 168 * 169 * @return int|float 170 */ 171 public function getTotalSpace() 172 { 173 $memc = $this->getMemcachedResource(); 174 $stats = $memc->getStats(); 175 if ($stats === false) { 176 throw new Exception\RuntimeException($memc->getResultMessage()); 177 } 178 179 $mem = array_pop($stats); 180 return $mem['limit_maxbytes']; 181 } 182 183 /* AvailableSpaceCapableInterface */ 184 185 /** 186 * Get available space in bytes 187 * 188 * @return int|float 189 */ 190 public function getAvailableSpace() 191 { 192 $memc = $this->getMemcachedResource(); 193 $stats = $memc->getStats(); 194 if ($stats === false) { 195 throw new Exception\RuntimeException($memc->getResultMessage()); 196 } 197 198 $mem = array_pop($stats); 199 return $mem['limit_maxbytes'] - $mem['bytes']; 200 } 201 202 /* reading */ 203 204 /** 205 * Internal method to get an item. 206 * 207 * @param string $normalizedKey 208 * @param bool $success 209 * @param mixed $casToken 210 * @return mixed Data on success, null on failure 211 * @throws Exception\ExceptionInterface 212 */ 213 protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) 214 { 215 $memc = $this->getMemcachedResource(); 216 $internalKey = $this->namespacePrefix . $normalizedKey; 217 218 if (func_num_args() > 2) { 219 $result = $memc->get($internalKey, null, $casToken); 220 } else { 221 $result = $memc->get($internalKey); 222 } 223 224 $success = true; 225 if ($result === false) { 226 $rsCode = $memc->getResultCode(); 227 if ($rsCode == MemcachedResource::RES_NOTFOUND) { 228 $result = null; 229 $success = false; 230 } elseif ($rsCode) { 231 $success = false; 232 throw $this->getExceptionByResultCode($rsCode); 233 } 234 } 235 236 return $result; 237 } 238 239 /** 240 * Internal method to get multiple items. 241 * 242 * @param array $normalizedKeys 243 * @return array Associative array of keys and values 244 * @throws Exception\ExceptionInterface 245 */ 246 protected function internalGetItems(array & $normalizedKeys) 247 { 248 $memc = $this->getMemcachedResource(); 249 250 foreach ($normalizedKeys as & $normalizedKey) { 251 $normalizedKey = $this->namespacePrefix . $normalizedKey; 252 } 253 254 $result = $memc->getMulti($normalizedKeys); 255 if ($result === false) { 256 throw $this->getExceptionByResultCode($memc->getResultCode()); 257 } 258 259 // remove namespace prefix from result 260 if ($result && $this->namespacePrefix !== '') { 261 $tmp = array(); 262 $nsPrefixLength = strlen($this->namespacePrefix); 263 foreach ($result as $internalKey => & $value) { 264 $tmp[substr($internalKey, $nsPrefixLength)] = & $value; 265 } 266 $result = $tmp; 267 } 268 269 return $result; 270 } 271 272 /** 273 * Internal method to test if an item exists. 274 * 275 * @param string $normalizedKey 276 * @return bool 277 * @throws Exception\ExceptionInterface 278 */ 279 protected function internalHasItem(& $normalizedKey) 280 { 281 $memc = $this->getMemcachedResource(); 282 $value = $memc->get($this->namespacePrefix . $normalizedKey); 283 if ($value === false) { 284 $rsCode = $memc->getResultCode(); 285 if ($rsCode == MemcachedResource::RES_SUCCESS) { 286 return true; 287 } elseif ($rsCode == MemcachedResource::RES_NOTFOUND) { 288 return false; 289 } else { 290 throw $this->getExceptionByResultCode($rsCode); 291 } 292 } 293 294 return true; 295 } 296 297 /** 298 * Internal method to test multiple items. 299 * 300 * @param array $normalizedKeys 301 * @return array Array of found keys 302 * @throws Exception\ExceptionInterface 303 */ 304 protected function internalHasItems(array & $normalizedKeys) 305 { 306 $memc = $this->getMemcachedResource(); 307 308 foreach ($normalizedKeys as & $normalizedKey) { 309 $normalizedKey = $this->namespacePrefix . $normalizedKey; 310 } 311 312 $result = $memc->getMulti($normalizedKeys); 313 if ($result === false) { 314 throw $this->getExceptionByResultCode($memc->getResultCode()); 315 } 316 317 // Convert to a simgle list 318 $result = array_keys($result); 319 320 // remove namespace prefix 321 if ($result && $this->namespacePrefix !== '') { 322 $nsPrefixLength = strlen($this->namespacePrefix); 323 foreach ($result as & $internalKey) { 324 $internalKey = substr($internalKey, $nsPrefixLength); 325 } 326 } 327 328 return $result; 329 } 330 331 /** 332 * Get metadata of multiple items 333 * 334 * @param array $normalizedKeys 335 * @return array Associative array of keys and metadata 336 * @throws Exception\ExceptionInterface 337 */ 338 protected function internalGetMetadatas(array & $normalizedKeys) 339 { 340 $memc = $this->getMemcachedResource(); 341 342 foreach ($normalizedKeys as & $normalizedKey) { 343 $normalizedKey = $this->namespacePrefix . $normalizedKey; 344 } 345 346 $result = $memc->getMulti($normalizedKeys); 347 if ($result === false) { 348 throw $this->getExceptionByResultCode($memc->getResultCode()); 349 } 350 351 // remove namespace prefix and use an empty array as metadata 352 if ($this->namespacePrefix !== '') { 353 $tmp = array(); 354 $nsPrefixLength = strlen($this->namespacePrefix); 355 foreach (array_keys($result) as $internalKey) { 356 $tmp[substr($internalKey, $nsPrefixLength)] = array(); 357 } 358 $result = $tmp; 359 } else { 360 foreach ($result as & $value) { 361 $value = array(); 362 } 363 } 364 365 return $result; 366 } 367 368 /* writing */ 369 370 /** 371 * Internal method to store an item. 372 * 373 * @param string $normalizedKey 374 * @param mixed $value 375 * @return bool 376 * @throws Exception\ExceptionInterface 377 */ 378 protected function internalSetItem(& $normalizedKey, & $value) 379 { 380 $memc = $this->getMemcachedResource(); 381 $expiration = $this->expirationTime(); 382 if (!$memc->set($this->namespacePrefix . $normalizedKey, $value, $expiration)) { 383 throw $this->getExceptionByResultCode($memc->getResultCode()); 384 } 385 386 return true; 387 } 388 389 /** 390 * Internal method to store multiple items. 391 * 392 * @param array $normalizedKeyValuePairs 393 * @return array Array of not stored keys 394 * @throws Exception\ExceptionInterface 395 */ 396 protected function internalSetItems(array & $normalizedKeyValuePairs) 397 { 398 $memc = $this->getMemcachedResource(); 399 $expiration = $this->expirationTime(); 400 401 $namespacedKeyValuePairs = array(); 402 foreach ($normalizedKeyValuePairs as $normalizedKey => & $value) { 403 $namespacedKeyValuePairs[$this->namespacePrefix . $normalizedKey] = & $value; 404 } 405 406 if (!$memc->setMulti($namespacedKeyValuePairs, $expiration)) { 407 throw $this->getExceptionByResultCode($memc->getResultCode()); 408 } 409 410 return array(); 411 } 412 413 /** 414 * Add an item. 415 * 416 * @param string $normalizedKey 417 * @param mixed $value 418 * @return bool 419 * @throws Exception\ExceptionInterface 420 */ 421 protected function internalAddItem(& $normalizedKey, & $value) 422 { 423 $memc = $this->getMemcachedResource(); 424 $expiration = $this->expirationTime(); 425 if (!$memc->add($this->namespacePrefix . $normalizedKey, $value, $expiration)) { 426 if ($memc->getResultCode() == MemcachedResource::RES_NOTSTORED) { 427 return false; 428 } 429 throw $this->getExceptionByResultCode($memc->getResultCode()); 430 } 431 432 return true; 433 } 434 435 /** 436 * Internal method to replace an existing item. 437 * 438 * @param string $normalizedKey 439 * @param mixed $value 440 * @return bool 441 * @throws Exception\ExceptionInterface 442 */ 443 protected function internalReplaceItem(& $normalizedKey, & $value) 444 { 445 $memc = $this->getMemcachedResource(); 446 $expiration = $this->expirationTime(); 447 if (!$memc->replace($this->namespacePrefix . $normalizedKey, $value, $expiration)) { 448 $rsCode = $memc->getResultCode(); 449 if ($rsCode == MemcachedResource::RES_NOTSTORED) { 450 return false; 451 } 452 throw $this->getExceptionByResultCode($rsCode); 453 } 454 455 return true; 456 } 457 458 /** 459 * Internal method to set an item only if token matches 460 * 461 * @param mixed $token 462 * @param string $normalizedKey 463 * @param mixed $value 464 * @return bool 465 * @throws Exception\ExceptionInterface 466 * @see getItem() 467 * @see setItem() 468 */ 469 protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) 470 { 471 $memc = $this->getMemcachedResource(); 472 $expiration = $this->expirationTime(); 473 $result = $memc->cas($token, $this->namespacePrefix . $normalizedKey, $value, $expiration); 474 475 if ($result === false) { 476 $rsCode = $memc->getResultCode(); 477 if ($rsCode !== 0 && $rsCode != MemcachedResource::RES_DATA_EXISTS) { 478 throw $this->getExceptionByResultCode($rsCode); 479 } 480 } 481 482 return $result; 483 } 484 485 /** 486 * Internal method to remove an item. 487 * 488 * @param string $normalizedKey 489 * @return bool 490 * @throws Exception\ExceptionInterface 491 */ 492 protected function internalRemoveItem(& $normalizedKey) 493 { 494 $memc = $this->getMemcachedResource(); 495 $result = $memc->delete($this->namespacePrefix . $normalizedKey); 496 497 if ($result === false) { 498 $rsCode = $memc->getResultCode(); 499 if ($rsCode == MemcachedResource::RES_NOTFOUND) { 500 return false; 501 } elseif ($rsCode != MemcachedResource::RES_SUCCESS) { 502 throw $this->getExceptionByResultCode($rsCode); 503 } 504 } 505 506 return true; 507 } 508 509 /** 510 * Internal method to remove multiple items. 511 * 512 * @param array $normalizedKeys 513 * @return array Array of not removed keys 514 * @throws Exception\ExceptionInterface 515 */ 516 protected function internalRemoveItems(array & $normalizedKeys) 517 { 518 // support for removing multiple items at once has been added in ext/memcached-2.0.0 519 if (static::$extMemcachedMajorVersion < 2) { 520 return parent::internalRemoveItems($normalizedKeys); 521 } 522 523 $memc = $this->getMemcachedResource(); 524 525 foreach ($normalizedKeys as & $normalizedKey) { 526 $normalizedKey = $this->namespacePrefix . $normalizedKey; 527 } 528 529 $rsCodes = $memc->deleteMulti($normalizedKeys); 530 531 $missingKeys = array(); 532 foreach ($rsCodes as $key => $rsCode) { 533 if ($rsCode !== true && $rsCode != MemcachedResource::RES_SUCCESS) { 534 if ($rsCode != MemcachedResource::RES_NOTFOUND) { 535 throw $this->getExceptionByResultCode($rsCode); 536 } 537 $missingKeys[] = $key; 538 } 539 } 540 541 // remove namespace prefix 542 if ($missingKeys && $this->namespacePrefix !== '') { 543 $nsPrefixLength = strlen($this->namespacePrefix); 544 foreach ($missingKeys as & $missingKey) { 545 $missingKey = substr($missingKey, $nsPrefixLength); 546 } 547 } 548 549 return $missingKeys; 550 } 551 552 /** 553 * Internal method to increment an item. 554 * 555 * @param string $normalizedKey 556 * @param int $value 557 * @return int|bool The new value on success, false on failure 558 * @throws Exception\ExceptionInterface 559 */ 560 protected function internalIncrementItem(& $normalizedKey, & $value) 561 { 562 $memc = $this->getMemcachedResource(); 563 $internalKey = $this->namespacePrefix . $normalizedKey; 564 $value = (int) $value; 565 $newValue = $memc->increment($internalKey, $value); 566 567 if ($newValue === false) { 568 $rsCode = $memc->getResultCode(); 569 570 // initial value 571 if ($rsCode == MemcachedResource::RES_NOTFOUND) { 572 $newValue = $value; 573 $memc->add($internalKey, $newValue, $this->expirationTime()); 574 $rsCode = $memc->getResultCode(); 575 } 576 577 if ($rsCode) { 578 throw $this->getExceptionByResultCode($rsCode); 579 } 580 } 581 582 return $newValue; 583 } 584 585 /** 586 * Internal method to decrement an item. 587 * 588 * @param string $normalizedKey 589 * @param int $value 590 * @return int|bool The new value on success, false on failure 591 * @throws Exception\ExceptionInterface 592 */ 593 protected function internalDecrementItem(& $normalizedKey, & $value) 594 { 595 $memc = $this->getMemcachedResource(); 596 $internalKey = $this->namespacePrefix . $normalizedKey; 597 $value = (int) $value; 598 $newValue = $memc->decrement($internalKey, $value); 599 600 if ($newValue === false) { 601 $rsCode = $memc->getResultCode(); 602 603 // initial value 604 if ($rsCode == MemcachedResource::RES_NOTFOUND) { 605 $newValue = -$value; 606 $memc->add($internalKey, $newValue, $this->expirationTime()); 607 $rsCode = $memc->getResultCode(); 608 } 609 610 if ($rsCode) { 611 throw $this->getExceptionByResultCode($rsCode); 612 } 613 } 614 615 return $newValue; 616 } 617 618 /* status */ 619 620 /** 621 * Internal method to get capabilities of this adapter 622 * 623 * @return Capabilities 624 */ 625 protected function internalGetCapabilities() 626 { 627 if ($this->capabilities === null) { 628 $this->capabilityMarker = new stdClass(); 629 $this->capabilities = new Capabilities( 630 $this, 631 $this->capabilityMarker, 632 array( 633 'supportedDatatypes' => array( 634 'NULL' => true, 635 'boolean' => true, 636 'integer' => true, 637 'double' => true, 638 'string' => true, 639 'array' => true, 640 'object' => 'object', 641 'resource' => false, 642 ), 643 'supportedMetadata' => array(), 644 'minTtl' => 1, 645 'maxTtl' => 0, 646 'staticTtl' => true, 647 'ttlPrecision' => 1, 648 'useRequestTime' => false, 649 'expiredRead' => false, 650 'maxKeyLength' => 255, 651 'namespaceIsPrefix' => true, 652 ) 653 ); 654 } 655 656 return $this->capabilities; 657 } 658 659 /* internal */ 660 661 /** 662 * Get expiration time by ttl 663 * 664 * Some storage commands involve sending an expiration value (relative to 665 * an item or to an operation requested by the client) to the server. In 666 * all such cases, the actual value sent may either be Unix time (number of 667 * seconds since January 1, 1970, as an integer), or a number of seconds 668 * starting from current time. In the latter case, this number of seconds 669 * may not exceed 60*60*24*30 (number of seconds in 30 days); if the 670 * expiration value is larger than that, the server will consider it to be 671 * real Unix time value rather than an offset from current time. 672 * 673 * @return int 674 */ 675 protected function expirationTime() 676 { 677 $ttl = $this->getOptions()->getTtl(); 678 if ($ttl > 2592000) { 679 return time() + $ttl; 680 } 681 return $ttl; 682 } 683 684 /** 685 * Generate exception based of memcached result code 686 * 687 * @param int $code 688 * @return Exception\RuntimeException 689 * @throws Exception\InvalidArgumentException On success code 690 */ 691 protected function getExceptionByResultCode($code) 692 { 693 switch ($code) { 694 case MemcachedResource::RES_SUCCESS: 695 throw new Exception\InvalidArgumentException( 696 "The result code '{$code}' (SUCCESS) isn't an error" 697 ); 698 699 default: 700 return new Exception\RuntimeException($this->getMemcachedResource()->getResultMessage()); 701 } 702 } 703} 704