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 APCIterator as BaseApcIterator; 13use stdClass; 14use Traversable; 15use Zend\Cache\Exception; 16use Zend\Cache\Storage\AvailableSpaceCapableInterface; 17use Zend\Cache\Storage\Capabilities; 18use Zend\Cache\Storage\ClearByNamespaceInterface; 19use Zend\Cache\Storage\ClearByPrefixInterface; 20use Zend\Cache\Storage\FlushableInterface; 21use Zend\Cache\Storage\IterableInterface; 22use Zend\Cache\Storage\TotalSpaceCapableInterface; 23 24class Apc extends AbstractAdapter implements 25 AvailableSpaceCapableInterface, 26 ClearByNamespaceInterface, 27 ClearByPrefixInterface, 28 FlushableInterface, 29 IterableInterface, 30 TotalSpaceCapableInterface 31{ 32 /** 33 * Buffered total space in bytes 34 * 35 * @var null|int|float 36 */ 37 protected $totalSpace; 38 39 /** 40 * Constructor 41 * 42 * @param null|array|Traversable|ApcOptions $options 43 * @throws Exception\ExceptionInterface 44 */ 45 public function __construct($options = null) 46 { 47 if (version_compare('3.1.6', phpversion('apc')) > 0) { 48 throw new Exception\ExtensionNotLoadedException("Missing ext/apc >= 3.1.6"); 49 } 50 51 $enabled = ini_get('apc.enabled'); 52 if (PHP_SAPI == 'cli') { 53 $enabled = $enabled && (bool) ini_get('apc.enable_cli'); 54 } 55 56 if (!$enabled) { 57 throw new Exception\ExtensionNotLoadedException( 58 "ext/apc is disabled - see 'apc.enabled' and 'apc.enable_cli'" 59 ); 60 } 61 62 parent::__construct($options); 63 } 64 65 /* options */ 66 67 /** 68 * Set options. 69 * 70 * @param array|Traversable|ApcOptions $options 71 * @return Apc 72 * @see getOptions() 73 */ 74 public function setOptions($options) 75 { 76 if (!$options instanceof ApcOptions) { 77 $options = new ApcOptions($options); 78 } 79 80 return parent::setOptions($options); 81 } 82 83 /** 84 * Get options. 85 * 86 * @return ApcOptions 87 * @see setOptions() 88 */ 89 public function getOptions() 90 { 91 if (!$this->options) { 92 $this->setOptions(new ApcOptions()); 93 } 94 return $this->options; 95 } 96 97 /* TotalSpaceCapableInterface */ 98 99 /** 100 * Get total space in bytes 101 * 102 * @return int|float 103 */ 104 public function getTotalSpace() 105 { 106 if ($this->totalSpace === null) { 107 $smaInfo = apc_sma_info(true); 108 $this->totalSpace = $smaInfo['num_seg'] * $smaInfo['seg_size']; 109 } 110 111 return $this->totalSpace; 112 } 113 114 /* AvailableSpaceCapableInterface */ 115 116 /** 117 * Get available space in bytes 118 * 119 * @return int|float 120 */ 121 public function getAvailableSpace() 122 { 123 $smaInfo = apc_sma_info(true); 124 return $smaInfo['avail_mem']; 125 } 126 127 /* IterableInterface */ 128 129 /** 130 * Get the storage iterator 131 * 132 * @return ApcIterator 133 */ 134 public function getIterator() 135 { 136 $options = $this->getOptions(); 137 $namespace = $options->getNamespace(); 138 $prefix = ''; 139 $pattern = null; 140 if ($namespace !== '') { 141 $prefix = $namespace . $options->getNamespaceSeparator(); 142 $pattern = '/^' . preg_quote($prefix, '/') . '/'; 143 } 144 145 $baseIt = new BaseApcIterator('user', $pattern, 0, 1, APC_LIST_ACTIVE); 146 return new ApcIterator($this, $baseIt, $prefix); 147 } 148 149 /* FlushableInterface */ 150 151 /** 152 * Flush the whole storage 153 * 154 * @return bool 155 */ 156 public function flush() 157 { 158 return apc_clear_cache('user'); 159 } 160 161 /* ClearByNamespaceInterface */ 162 163 /** 164 * Remove items by given namespace 165 * 166 * @param string $namespace 167 * @return bool 168 */ 169 public function clearByNamespace($namespace) 170 { 171 $namespace = (string) $namespace; 172 if ($namespace === '') { 173 throw new Exception\InvalidArgumentException('No namespace given'); 174 } 175 176 $options = $this->getOptions(); 177 $prefix = $namespace . $options->getNamespaceSeparator(); 178 $pattern = '/^' . preg_quote($prefix, '/') . '/'; 179 return apc_delete(new BaseApcIterator('user', $pattern, 0, 1, APC_LIST_ACTIVE)); 180 } 181 182 /* ClearByPrefixInterface */ 183 184 /** 185 * Remove items matching given prefix 186 * 187 * @param string $prefix 188 * @return bool 189 */ 190 public function clearByPrefix($prefix) 191 { 192 $prefix = (string) $prefix; 193 if ($prefix === '') { 194 throw new Exception\InvalidArgumentException('No prefix given'); 195 } 196 197 $options = $this->getOptions(); 198 $namespace = $options->getNamespace(); 199 $nsPrefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 200 $pattern = '/^' . preg_quote($nsPrefix . $prefix, '/') . '/'; 201 return apc_delete(new BaseApcIterator('user', $pattern, 0, 1, APC_LIST_ACTIVE)); 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 $options = $this->getOptions(); 218 $namespace = $options->getNamespace(); 219 $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 220 $internalKey = $prefix . $normalizedKey; 221 $result = apc_fetch($internalKey, $success); 222 223 if (!$success) { 224 return; 225 } 226 227 $casToken = $result; 228 return $result; 229 } 230 231 /** 232 * Internal method to get multiple items. 233 * 234 * @param array $normalizedKeys 235 * @return array Associative array of keys and values 236 * @throws Exception\ExceptionInterface 237 */ 238 protected function internalGetItems(array & $normalizedKeys) 239 { 240 $options = $this->getOptions(); 241 $namespace = $options->getNamespace(); 242 if ($namespace === '') { 243 return apc_fetch($normalizedKeys); 244 } 245 246 $prefix = $namespace . $options->getNamespaceSeparator(); 247 $internalKeys = array(); 248 foreach ($normalizedKeys as $normalizedKey) { 249 $internalKeys[] = $prefix . $normalizedKey; 250 } 251 252 $fetch = apc_fetch($internalKeys); 253 254 // remove namespace prefix 255 $prefixL = strlen($prefix); 256 $result = array(); 257 foreach ($fetch as $internalKey => & $value) { 258 $result[substr($internalKey, $prefixL)] = $value; 259 } 260 261 return $result; 262 } 263 264 /** 265 * Internal method to test if an item exists. 266 * 267 * @param string $normalizedKey 268 * @return bool 269 * @throws Exception\ExceptionInterface 270 */ 271 protected function internalHasItem(& $normalizedKey) 272 { 273 $options = $this->getOptions(); 274 $namespace = $options->getNamespace(); 275 $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 276 return apc_exists($prefix . $normalizedKey); 277 } 278 279 /** 280 * Internal method to test multiple items. 281 * 282 * @param array $normalizedKeys 283 * @return array Array of found keys 284 * @throws Exception\ExceptionInterface 285 */ 286 protected function internalHasItems(array & $normalizedKeys) 287 { 288 $options = $this->getOptions(); 289 $namespace = $options->getNamespace(); 290 if ($namespace === '') { 291 // array_filter with no callback will remove entries equal to FALSE 292 return array_keys(array_filter(apc_exists($normalizedKeys))); 293 } 294 295 $prefix = $namespace . $options->getNamespaceSeparator(); 296 $internalKeys = array(); 297 foreach ($normalizedKeys as $normalizedKey) { 298 $internalKeys[] = $prefix . $normalizedKey; 299 } 300 301 $exists = apc_exists($internalKeys); 302 $result = array(); 303 $prefixL = strlen($prefix); 304 foreach ($exists as $internalKey => $bool) { 305 if ($bool === true) { 306 $result[] = substr($internalKey, $prefixL); 307 } 308 } 309 310 return $result; 311 } 312 313 /** 314 * Get metadata of an item. 315 * 316 * @param string $normalizedKey 317 * @return array|bool Metadata on success, false on failure 318 * @throws Exception\ExceptionInterface 319 */ 320 protected function internalGetMetadata(& $normalizedKey) 321 { 322 $options = $this->getOptions(); 323 $namespace = $options->getNamespace(); 324 $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 325 $internalKey = $prefix . $normalizedKey; 326 327 // @see http://pecl.php.net/bugs/bug.php?id=22564 328 if (!apc_exists($internalKey)) { 329 $metadata = false; 330 } else { 331 $format = APC_ITER_ALL ^ APC_ITER_VALUE ^ APC_ITER_TYPE ^ APC_ITER_REFCOUNT; 332 $regexp = '/^' . preg_quote($internalKey, '/') . '$/'; 333 $it = new BaseApcIterator('user', $regexp, $format, 100, APC_LIST_ACTIVE); 334 $metadata = $it->current(); 335 } 336 337 if (!$metadata) { 338 return false; 339 } 340 341 $this->normalizeMetadata($metadata); 342 return $metadata; 343 } 344 345 /** 346 * Get metadata of multiple items 347 * 348 * @param array $normalizedKeys 349 * @return array Associative array of keys and metadata 350 * 351 * @triggers getMetadatas.pre(PreEvent) 352 * @triggers getMetadatas.post(PostEvent) 353 * @triggers getMetadatas.exception(ExceptionEvent) 354 */ 355 protected function internalGetMetadatas(array & $normalizedKeys) 356 { 357 $keysRegExp = array(); 358 foreach ($normalizedKeys as $normalizedKey) { 359 $keysRegExp[] = preg_quote($normalizedKey, '/'); 360 } 361 362 $options = $this->getOptions(); 363 $namespace = $options->getNamespace(); 364 $prefixL = 0; 365 366 if ($namespace === '') { 367 $pattern = '/^(' . implode('|', $keysRegExp) . ')' . '$/'; 368 } else { 369 $prefix = $namespace . $options->getNamespaceSeparator(); 370 $prefixL = strlen($prefix); 371 $pattern = '/^' . preg_quote($prefix, '/') . '(' . implode('|', $keysRegExp) . ')' . '$/'; 372 } 373 374 $format = APC_ITER_ALL ^ APC_ITER_VALUE ^ APC_ITER_TYPE ^ APC_ITER_REFCOUNT; 375 $it = new BaseApcIterator('user', $pattern, $format, 100, APC_LIST_ACTIVE); 376 $result = array(); 377 foreach ($it as $internalKey => $metadata) { 378 // @see http://pecl.php.net/bugs/bug.php?id=22564 379 if (!apc_exists($internalKey)) { 380 continue; 381 } 382 383 $this->normalizeMetadata($metadata); 384 $result[substr($internalKey, $prefixL)] = $metadata; 385 } 386 387 return $result; 388 } 389 390 /* writing */ 391 392 /** 393 * Internal method to store an item. 394 * 395 * @param string $normalizedKey 396 * @param mixed $value 397 * @return bool 398 * @throws Exception\ExceptionInterface 399 */ 400 protected function internalSetItem(& $normalizedKey, & $value) 401 { 402 $options = $this->getOptions(); 403 $namespace = $options->getNamespace(); 404 $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 405 $internalKey = $prefix . $normalizedKey; 406 $ttl = $options->getTtl(); 407 408 if (!apc_store($internalKey, $value, $ttl)) { 409 $type = is_object($value) ? get_class($value) : gettype($value); 410 throw new Exception\RuntimeException( 411 "apc_store('{$internalKey}', <{$type}>, {$ttl}) failed" 412 ); 413 } 414 415 return true; 416 } 417 418 /** 419 * Internal method to store multiple items. 420 * 421 * @param array $normalizedKeyValuePairs 422 * @return array Array of not stored keys 423 * @throws Exception\ExceptionInterface 424 */ 425 protected function internalSetItems(array & $normalizedKeyValuePairs) 426 { 427 $options = $this->getOptions(); 428 $namespace = $options->getNamespace(); 429 if ($namespace === '') { 430 return array_keys(apc_store($normalizedKeyValuePairs, null, $options->getTtl())); 431 } 432 433 $prefix = $namespace . $options->getNamespaceSeparator(); 434 $internalKeyValuePairs = array(); 435 foreach ($normalizedKeyValuePairs as $normalizedKey => &$value) { 436 $internalKey = $prefix . $normalizedKey; 437 $internalKeyValuePairs[$internalKey] = &$value; 438 } 439 440 $failedKeys = apc_store($internalKeyValuePairs, null, $options->getTtl()); 441 $failedKeys = array_keys($failedKeys); 442 443 // remove prefix 444 $prefixL = strlen($prefix); 445 foreach ($failedKeys as & $key) { 446 $key = substr($key, $prefixL); 447 } 448 449 return $failedKeys; 450 } 451 452 /** 453 * Add an item. 454 * 455 * @param string $normalizedKey 456 * @param mixed $value 457 * @return bool 458 * @throws Exception\ExceptionInterface 459 */ 460 protected function internalAddItem(& $normalizedKey, & $value) 461 { 462 $options = $this->getOptions(); 463 $namespace = $options->getNamespace(); 464 $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 465 $internalKey = $prefix . $normalizedKey; 466 $ttl = $options->getTtl(); 467 468 if (!apc_add($internalKey, $value, $ttl)) { 469 if (apc_exists($internalKey)) { 470 return false; 471 } 472 473 $type = is_object($value) ? get_class($value) : gettype($value); 474 throw new Exception\RuntimeException( 475 "apc_add('{$internalKey}', <{$type}>, {$ttl}) failed" 476 ); 477 } 478 479 return true; 480 } 481 482 /** 483 * Internal method to add multiple items. 484 * 485 * @param array $normalizedKeyValuePairs 486 * @return array Array of not stored keys 487 * @throws Exception\ExceptionInterface 488 */ 489 protected function internalAddItems(array & $normalizedKeyValuePairs) 490 { 491 $options = $this->getOptions(); 492 $namespace = $options->getNamespace(); 493 if ($namespace === '') { 494 return array_keys(apc_add($normalizedKeyValuePairs, null, $options->getTtl())); 495 } 496 497 $prefix = $namespace . $options->getNamespaceSeparator(); 498 $internalKeyValuePairs = array(); 499 foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { 500 $internalKey = $prefix . $normalizedKey; 501 $internalKeyValuePairs[$internalKey] = $value; 502 } 503 504 $failedKeys = apc_add($internalKeyValuePairs, null, $options->getTtl()); 505 $failedKeys = array_keys($failedKeys); 506 507 // remove prefix 508 $prefixL = strlen($prefix); 509 foreach ($failedKeys as & $key) { 510 $key = substr($key, $prefixL); 511 } 512 513 return $failedKeys; 514 } 515 516 /** 517 * Internal method to replace an existing item. 518 * 519 * @param string $normalizedKey 520 * @param mixed $value 521 * @return bool 522 * @throws Exception\ExceptionInterface 523 */ 524 protected function internalReplaceItem(& $normalizedKey, & $value) 525 { 526 $options = $this->getOptions(); 527 $namespace = $options->getNamespace(); 528 $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 529 $internalKey = $prefix . $normalizedKey; 530 531 if (!apc_exists($internalKey)) { 532 return false; 533 } 534 535 $ttl = $options->getTtl(); 536 if (!apc_store($internalKey, $value, $ttl)) { 537 $type = is_object($value) ? get_class($value) : gettype($value); 538 throw new Exception\RuntimeException( 539 "apc_store('{$internalKey}', <{$type}>, {$ttl}) failed" 540 ); 541 } 542 543 return true; 544 } 545 546 /** 547 * Internal method to remove an item. 548 * 549 * @param string $normalizedKey 550 * @return bool 551 * @throws Exception\ExceptionInterface 552 */ 553 protected function internalRemoveItem(& $normalizedKey) 554 { 555 $options = $this->getOptions(); 556 $namespace = $options->getNamespace(); 557 $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 558 return apc_delete($prefix . $normalizedKey); 559 } 560 561 /** 562 * Internal method to remove multiple items. 563 * 564 * @param array $normalizedKeys 565 * @return array Array of not removed keys 566 * @throws Exception\ExceptionInterface 567 */ 568 protected function internalRemoveItems(array & $normalizedKeys) 569 { 570 $options = $this->getOptions(); 571 $namespace = $options->getNamespace(); 572 if ($namespace === '') { 573 return apc_delete($normalizedKeys); 574 } 575 576 $prefix = $namespace . $options->getNamespaceSeparator(); 577 $internalKeys = array(); 578 foreach ($normalizedKeys as $normalizedKey) { 579 $internalKeys[] = $prefix . $normalizedKey; 580 } 581 582 $failedKeys = apc_delete($internalKeys); 583 584 // remove prefix 585 $prefixL = strlen($prefix); 586 foreach ($failedKeys as & $key) { 587 $key = substr($key, $prefixL); 588 } 589 590 return $failedKeys; 591 } 592 593 /** 594 * Internal method to increment an item. 595 * 596 * @param string $normalizedKey 597 * @param int $value 598 * @return int|bool The new value on success, false on failure 599 * @throws Exception\ExceptionInterface 600 */ 601 protected function internalIncrementItem(& $normalizedKey, & $value) 602 { 603 $options = $this->getOptions(); 604 $namespace = $options->getNamespace(); 605 $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 606 $internalKey = $prefix . $normalizedKey; 607 $value = (int) $value; 608 $newValue = apc_inc($internalKey, $value); 609 610 // initial value 611 if ($newValue === false) { 612 $ttl = $options->getTtl(); 613 $newValue = $value; 614 if (!apc_add($internalKey, $newValue, $ttl)) { 615 throw new Exception\RuntimeException( 616 "apc_add('{$internalKey}', {$newValue}, {$ttl}) failed" 617 ); 618 } 619 } 620 621 return $newValue; 622 } 623 624 /** 625 * Internal method to decrement an item. 626 * 627 * @param string $normalizedKey 628 * @param int $value 629 * @return int|bool The new value on success, false on failure 630 * @throws Exception\ExceptionInterface 631 */ 632 protected function internalDecrementItem(& $normalizedKey, & $value) 633 { 634 $options = $this->getOptions(); 635 $namespace = $options->getNamespace(); 636 $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); 637 $internalKey = $prefix . $normalizedKey; 638 $value = (int) $value; 639 $newValue = apc_dec($internalKey, $value); 640 641 // initial value 642 if ($newValue === false) { 643 $ttl = $options->getTtl(); 644 $newValue = -$value; 645 if (!apc_add($internalKey, $newValue, $ttl)) { 646 throw new Exception\RuntimeException( 647 "apc_add('{$internalKey}', {$newValue}, {$ttl}) failed" 648 ); 649 } 650 } 651 652 return $newValue; 653 } 654 655 /* status */ 656 657 /** 658 * Internal method to get capabilities of this adapter 659 * 660 * @return Capabilities 661 */ 662 protected function internalGetCapabilities() 663 { 664 if ($this->capabilities === null) { 665 $marker = new stdClass(); 666 $capabilities = new Capabilities( 667 $this, 668 $marker, 669 array( 670 'supportedDatatypes' => array( 671 'NULL' => true, 672 'boolean' => true, 673 'integer' => true, 674 'double' => true, 675 'string' => true, 676 'array' => true, 677 'object' => 'object', 678 'resource' => false, 679 ), 680 'supportedMetadata' => array( 681 'internal_key', 682 'atime', 'ctime', 'mtime', 'rtime', 683 'size', 'hits', 'ttl', 684 ), 685 'minTtl' => 1, 686 'maxTtl' => 0, 687 'staticTtl' => true, 688 'ttlPrecision' => 1, 689 'useRequestTime' => (bool) ini_get('apc.use_request_time'), 690 'expiredRead' => false, 691 'maxKeyLength' => 5182, 692 'namespaceIsPrefix' => true, 693 'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(), 694 ) 695 ); 696 697 // update namespace separator on change option 698 $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { 699 $params = $event->getParams(); 700 701 if (isset($params['namespace_separator'])) { 702 $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); 703 } 704 }); 705 706 $this->capabilities = $capabilities; 707 $this->capabilityMarker = $marker; 708 } 709 710 return $this->capabilities; 711 } 712 713 /* internal */ 714 715 /** 716 * Normalize metadata to work with APC 717 * 718 * @param array $metadata 719 * @return void 720 */ 721 protected function normalizeMetadata(array & $metadata) 722 { 723 $apcMetadata = $metadata; 724 $metadata = array( 725 'internal_key' => isset($metadata['key']) ? $metadata['key'] : $metadata['info'], 726 'atime' => isset($metadata['access_time']) ? $metadata['access_time'] : $metadata['atime'], 727 'ctime' => isset($metadata['creation_time']) ? $metadata['creation_time'] : $metadata['ctime'], 728 'mtime' => isset($metadata['modified_time']) ? $metadata['modified_time'] : $metadata['mtime'], 729 'rtime' => isset($metadata['deletion_time']) ? $metadata['deletion_time'] : $metadata['dtime'], 730 'size' => $metadata['mem_size'], 731 'hits' => isset($metadata['nhits']) ? $metadata['nhits'] : $metadata['num_hits'], 732 'ttl' => $metadata['ttl'], 733 ); 734 } 735 736 /** 737 * Internal method to set an item only if token matches 738 * 739 * @param mixed $token 740 * @param string $normalizedKey 741 * @param mixed $value 742 * @return bool 743 * @see getItem() 744 * @see setItem() 745 */ 746 protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) 747 { 748 if (is_int($token) && is_int($value)) { 749 return apc_cas($normalizedKey, $token, $value); 750 } 751 752 return parent::internalCheckAndSetItem($token, $normalizedKey, $value); 753 } 754} 755