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 ArrayAccess; 13use Memcache as MemcacheResource; 14use Traversable; 15use Zend\Cache\Exception; 16use Zend\Stdlib\ArrayUtils; 17 18/** 19 * This is a resource manager for memcache 20 */ 21class MemcacheResourceManager 22{ 23 /** 24 * Registered resources 25 * 26 * @var array 27 */ 28 protected $resources = array(); 29 30 /** 31 * Default server values per resource 32 * 33 * @var array 34 */ 35 protected $serverDefaults = array(); 36 37 /** 38 * Failure callback per resource 39 * 40 * @var callable[] 41 */ 42 protected $failureCallbacks = array(); 43 44 /** 45 * Check if a resource exists 46 * 47 * @param string $id 48 * @return bool 49 */ 50 public function hasResource($id) 51 { 52 return isset($this->resources[$id]); 53 } 54 55 /** 56 * Gets a memcache resource 57 * 58 * @param string $id 59 * @return MemcacheResource 60 * @throws Exception\RuntimeException 61 */ 62 public function getResource($id) 63 { 64 if (!$this->hasResource($id)) { 65 throw new Exception\RuntimeException("No resource with id '{$id}'"); 66 } 67 68 $resource = $this->resources[$id]; 69 if ($resource instanceof MemcacheResource) { 70 return $resource; 71 } 72 73 $memc = new MemcacheResource(); 74 $this->setResourceAutoCompressThreshold( 75 $memc, 76 $resource['auto_compress_threshold'], 77 $resource['auto_compress_min_savings'] 78 ); 79 foreach ($resource['servers'] as $server) { 80 $this->addServerToResource( 81 $memc, 82 $server, 83 $this->serverDefaults[$id], 84 $this->failureCallbacks[$id] 85 ); 86 } 87 88 // buffer and return 89 $this->resources[$id] = $memc; 90 return $memc; 91 } 92 93 /** 94 * Set a resource 95 * 96 * @param string $id 97 * @param array|Traversable|MemcacheResource $resource 98 * @param callable $failureCallback 99 * @param array|Traversable $serverDefaults 100 * @return MemcacheResourceManager 101 */ 102 public function setResource($id, $resource, $failureCallback = null, $serverDefaults = array()) 103 { 104 $id = (string) $id; 105 106 if ($serverDefaults instanceof Traversable) { 107 $serverDefaults = ArrayUtils::iteratorToArray($serverDefaults); 108 } elseif (!is_array($serverDefaults)) { 109 throw new Exception\InvalidArgumentException( 110 'ServerDefaults must be an instance Traversable or an array' 111 ); 112 } 113 114 if (!($resource instanceof MemcacheResource)) { 115 if ($resource instanceof Traversable) { 116 $resource = ArrayUtils::iteratorToArray($resource); 117 } elseif (!is_array($resource)) { 118 throw new Exception\InvalidArgumentException( 119 'Resource must be an instance of Memcache or an array or Traversable' 120 ); 121 } 122 123 if (isset($resource['server_defaults'])) { 124 $serverDefaults = array_merge($serverDefaults, $resource['server_defaults']); 125 unset($resource['server_defaults']); 126 } 127 128 $resourceOptions = array( 129 'servers' => array(), 130 'auto_compress_threshold' => null, 131 'auto_compress_min_savings' => null, 132 ); 133 $resource = array_merge($resourceOptions, $resource); 134 135 // normalize and validate params 136 $this->normalizeAutoCompressThreshold( 137 $resource['auto_compress_threshold'], 138 $resource['auto_compress_min_savings'] 139 ); 140 $this->normalizeServers($resource['servers']); 141 } 142 143 $this->normalizeServerDefaults($serverDefaults); 144 145 $this->resources[$id] = $resource; 146 $this->failureCallbacks[$id] = $failureCallback; 147 $this->serverDefaults[$id] = $serverDefaults; 148 149 return $this; 150 } 151 152 /** 153 * Remove a resource 154 * 155 * @param string $id 156 * @return MemcacheResourceManager 157 */ 158 public function removeResource($id) 159 { 160 unset($this->resources[$id]); 161 return $this; 162 } 163 164 /** 165 * Normalize compress threshold options 166 * 167 * @param int|string|array|ArrayAccess $threshold 168 * @param float|string $minSavings 169 */ 170 protected function normalizeAutoCompressThreshold(& $threshold, & $minSavings) 171 { 172 if (is_array($threshold) || ($threshold instanceof ArrayAccess)) { 173 $tmpThreshold = (isset($threshold['threshold'])) ? $threshold['threshold'] : null; 174 $minSavings = (isset($threshold['min_savings'])) ? $threshold['min_savings'] : $minSavings; 175 $threshold = $tmpThreshold; 176 } 177 if (isset($threshold)) { 178 $threshold = (int) $threshold; 179 } 180 if (isset($minSavings)) { 181 $minSavings = (float) $minSavings; 182 } 183 } 184 185 /** 186 * Set compress threshold on a Memcache resource 187 * 188 * @param MemcacheResource $resource 189 * @param int $threshold 190 * @param float $minSavings 191 */ 192 protected function setResourceAutoCompressThreshold(MemcacheResource $resource, $threshold, $minSavings) 193 { 194 if (!isset($threshold)) { 195 return; 196 } 197 if (isset($minSavings)) { 198 $resource->setCompressThreshold($threshold, $minSavings); 199 } else { 200 $resource->setCompressThreshold($threshold); 201 } 202 } 203 204 /** 205 * Get compress threshold 206 * 207 * @param string $id 208 * @return int|null 209 * @throws \Zend\Cache\Exception\RuntimeException 210 */ 211 public function getAutoCompressThreshold($id) 212 { 213 if (!$this->hasResource($id)) { 214 throw new Exception\RuntimeException("No resource with id '{$id}'"); 215 } 216 217 $resource = & $this->resources[$id]; 218 if ($resource instanceof MemcacheResource) { 219 // Cannot get options from Memcache resource once created 220 throw new Exception\RuntimeException("Cannot get compress threshold once resource is created"); 221 } 222 return $resource['auto_compress_threshold']; 223 } 224 225 /** 226 * Set compress threshold 227 * 228 * @param string $id 229 * @param int|string|array|ArrayAccess|null $threshold 230 * @param float|string|bool $minSavings 231 * @return MemcacheResourceManager 232 */ 233 public function setAutoCompressThreshold($id, $threshold, $minSavings = false) 234 { 235 if (!$this->hasResource($id)) { 236 return $this->setResource($id, array( 237 'auto_compress_threshold' => $threshold, 238 )); 239 } 240 241 $this->normalizeAutoCompressThreshold($threshold, $minSavings); 242 243 $resource = & $this->resources[$id]; 244 if ($resource instanceof MemcacheResource) { 245 $this->setResourceAutoCompressThreshold($resource, $threshold, $minSavings); 246 } else { 247 $resource['auto_compress_threshold'] = $threshold; 248 if ($minSavings !== false) { 249 $resource['auto_compress_min_savings'] = $minSavings; 250 } 251 } 252 return $this; 253 } 254 255 /** 256 * Get compress min savings 257 * 258 * @param string $id 259 * @return float|null 260 * @throws Exception\RuntimeException 261 */ 262 public function getAutoCompressMinSavings($id) 263 { 264 if (!$this->hasResource($id)) { 265 throw new Exception\RuntimeException("No resource with id '{$id}'"); 266 } 267 268 $resource = & $this->resources[$id]; 269 if ($resource instanceof MemcacheResource) { 270 // Cannot get options from Memcache resource once created 271 throw new Exception\RuntimeException("Cannot get compress min savings once resource is created"); 272 } 273 return $resource['auto_compress_min_savings']; 274 } 275 276 /** 277 * Set compress min savings 278 * 279 * @param string $id 280 * @param float|string|null $minSavings 281 * @return MemcacheResourceManager 282 * @throws \Zend\Cache\Exception\RuntimeException 283 */ 284 public function setAutoCompressMinSavings($id, $minSavings) 285 { 286 if (!$this->hasResource($id)) { 287 return $this->setResource($id, array( 288 'auto_compress_min_savings' => $minSavings, 289 )); 290 } 291 292 $minSavings = (float) $minSavings; 293 294 $resource = & $this->resources[$id]; 295 if ($resource instanceof MemcacheResource) { 296 throw new Exception\RuntimeException( 297 "Cannot set compress min savings without a threshold value once a resource is created" 298 ); 299 } else { 300 $resource['auto_compress_min_savings'] = $minSavings; 301 } 302 return $this; 303 } 304 305 /** 306 * Set default server values 307 * array( 308 * 'persistent' => <persistent>, 'weight' => <weight>, 309 * 'timeout' => <timeout>, 'retry_interval' => <retryInterval>, 310 * ) 311 * @param string $id 312 * @param array $serverDefaults 313 * @return MemcacheResourceManager 314 */ 315 public function setServerDefaults($id, array $serverDefaults) 316 { 317 if (!$this->hasResource($id)) { 318 return $this->setResource($id, array( 319 'server_defaults' => $serverDefaults 320 )); 321 } 322 323 $this->normalizeServerDefaults($serverDefaults); 324 $this->serverDefaults[$id] = $serverDefaults; 325 326 return $this; 327 } 328 329 /** 330 * Get default server values 331 * 332 * @param string $id 333 * @return array 334 * @throws Exception\RuntimeException 335 */ 336 public function getServerDefaults($id) 337 { 338 if (!isset($this->serverDefaults[$id])) { 339 throw new Exception\RuntimeException("No resource with id '{$id}'"); 340 } 341 return $this->serverDefaults[$id]; 342 } 343 344 /** 345 * @param array $serverDefaults 346 * @throws Exception\InvalidArgumentException 347 */ 348 protected function normalizeServerDefaults(& $serverDefaults) 349 { 350 if (!is_array($serverDefaults) && !($serverDefaults instanceof Traversable)) { 351 throw new Exception\InvalidArgumentException( 352 "Server defaults must be an array or an instance of Traversable" 353 ); 354 } 355 356 // Defaults 357 $result = array( 358 'persistent' => true, 359 'weight' => 1, 360 'timeout' => 1, // seconds 361 'retry_interval' => 15, // seconds 362 ); 363 364 foreach ($serverDefaults as $key => $value) { 365 switch ($key) { 366 case 'persistent': 367 $value = (bool) $value; 368 break; 369 case 'weight': 370 case 'timeout': 371 case 'retry_interval': 372 $value = (int) $value; 373 break; 374 } 375 $result[$key] = $value; 376 } 377 378 $serverDefaults = $result; 379 } 380 381 /** 382 * Set callback for server connection failures 383 * 384 * @param string $id 385 * @param callable|null $failureCallback 386 * @return MemcacheResourceManager 387 */ 388 public function setFailureCallback($id, $failureCallback) 389 { 390 if (!$this->hasResource($id)) { 391 return $this->setResource($id, array(), $failureCallback); 392 } 393 394 $this->failureCallbacks[$id] = $failureCallback; 395 return $this; 396 } 397 398 /** 399 * Get callback for server connection failures 400 * 401 * @param string $id 402 * @return callable 403 * @throws Exception\RuntimeException 404 */ 405 public function getFailureCallback($id) 406 { 407 if (!isset($this->failureCallbacks[$id])) { 408 throw new Exception\RuntimeException("No resource with id '{$id}'"); 409 } 410 return $this->failureCallbacks[$id]; 411 } 412 413 /** 414 * Get servers 415 * 416 * @param string $id 417 * @throws Exception\RuntimeException 418 * @return array array('host' => <host>, 'port' => <port>, 'weight' => <weight>) 419 */ 420 public function getServers($id) 421 { 422 if (!$this->hasResource($id)) { 423 throw new Exception\RuntimeException("No resource with id '{$id}'"); 424 } 425 426 $resource = & $this->resources[$id]; 427 if ($resource instanceof MemcacheResource) { 428 throw new Exception\RuntimeException("Cannot get server list once resource is created"); 429 } 430 return $resource['servers']; 431 } 432 433 /** 434 * Add servers 435 * 436 * @param string $id 437 * @param string|array $servers 438 * @return MemcacheResourceManager 439 */ 440 public function addServers($id, $servers) 441 { 442 if (!$this->hasResource($id)) { 443 return $this->setResource($id, array( 444 'servers' => $servers 445 )); 446 } 447 448 $this->normalizeServers($servers); 449 450 $resource = & $this->resources[$id]; 451 if ($resource instanceof MemcacheResource) { 452 foreach ($servers as $server) { 453 $this->addServerToResource( 454 $resource, 455 $server, 456 $this->serverDefaults[$id], 457 $this->failureCallbacks[$id] 458 ); 459 } 460 } else { 461 // don't add servers twice 462 $resource['servers'] = array_merge( 463 $resource['servers'], 464 array_udiff($servers, $resource['servers'], array($this, 'compareServers')) 465 ); 466 } 467 468 return $this; 469 } 470 471 /** 472 * Add one server 473 * 474 * @param string $id 475 * @param string|array $server 476 * @return MemcacheResourceManager 477 */ 478 public function addServer($id, $server) 479 { 480 return $this->addServers($id, array($server)); 481 } 482 483 /** 484 * @param MemcacheResource $resource 485 * @param array $server 486 * @param array $serverDefaults 487 * @param callable|null $failureCallback 488 */ 489 protected function addServerToResource( 490 MemcacheResource $resource, 491 array $server, 492 array $serverDefaults, 493 $failureCallback 494 ) { 495 // Apply server defaults 496 $server = array_merge($serverDefaults, $server); 497 498 // Reorder parameters 499 $params = array( 500 $server['host'], 501 $server['port'], 502 $server['persistent'], 503 $server['weight'], 504 $server['timeout'], 505 $server['retry_interval'], 506 $server['status'], 507 ); 508 if (isset($failureCallback)) { 509 $params[] = $failureCallback; 510 } 511 call_user_func_array(array($resource, 'addServer'), $params); 512 } 513 514 /** 515 * Normalize a list of servers into the following format: 516 * array(array('host' => <host>, 'port' => <port>, 'weight' => <weight>)[, ...]) 517 * 518 * @param string|array $servers 519 */ 520 protected function normalizeServers(& $servers) 521 { 522 if (is_string($servers)) { 523 // Convert string into a list of servers 524 $servers = explode(',', $servers); 525 } 526 527 $result = array(); 528 foreach ($servers as $server) { 529 $this->normalizeServer($server); 530 $result[$server['host'] . ':' . $server['port']] = $server; 531 } 532 533 $servers = array_values($result); 534 } 535 536 /** 537 * Normalize one server into the following format: 538 * array( 539 * 'host' => <host>, 'port' => <port>, 'weight' => <weight>, 540 * 'status' => <status>, 'persistent' => <persistent>, 541 * 'timeout' => <timeout>, 'retry_interval' => <retryInterval>, 542 * ) 543 * 544 * @param string|array $server 545 * @throws Exception\InvalidArgumentException 546 */ 547 protected function normalizeServer(& $server) 548 { 549 // WARNING: The order of this array is important. 550 // Used for converting an ordered array to a keyed array. 551 // Append new options, do not insert or you will break BC. 552 $sTmp = array( 553 'host' => null, 554 'port' => 11211, 555 'weight' => null, 556 'status' => true, 557 'persistent' => null, 558 'timeout' => null, 559 'retry_interval' => null, 560 ); 561 562 // convert a single server into an array 563 if ($server instanceof Traversable) { 564 $server = ArrayUtils::iteratorToArray($server); 565 } 566 567 if (is_array($server)) { 568 if (isset($server[0])) { 569 // Convert ordered array to keyed array 570 // array(<host>[, <port>[, <weight>[, <status>[, <persistent>[, <timeout>[, <retryInterval>]]]]]]) 571 $server = array_combine( 572 array_slice(array_keys($sTmp), 0, count($server)), 573 $server 574 ); 575 } 576 $sTmp = array_merge($sTmp, $server); 577 } elseif (is_string($server)) { 578 // parse server from URI host{:?port}{?weight} 579 $server = trim($server); 580 if (strpos($server, '://') === false) { 581 $server = 'tcp://' . $server; 582 } 583 584 $urlParts = parse_url($server); 585 if (!$urlParts) { 586 throw new Exception\InvalidArgumentException("Invalid server given"); 587 } 588 589 $sTmp = array_merge($sTmp, array_intersect_key($urlParts, $sTmp)); 590 if (isset($urlParts['query'])) { 591 $query = null; 592 parse_str($urlParts['query'], $query); 593 $sTmp = array_merge($sTmp, array_intersect_key($query, $sTmp)); 594 } 595 } 596 597 if (!$sTmp['host']) { 598 throw new Exception\InvalidArgumentException('Missing required server host'); 599 } 600 601 // Filter values 602 foreach ($sTmp as $key => $value) { 603 if (isset($value)) { 604 switch ($key) { 605 case 'host': 606 $value = (string) $value; 607 break; 608 case 'status': 609 case 'persistent': 610 $value = (bool) $value; 611 break; 612 case 'port': 613 case 'weight': 614 case 'timeout': 615 case 'retry_interval': 616 $value = (int) $value; 617 break; 618 } 619 } 620 $sTmp[$key] = $value; 621 } 622 $sTmp = array_filter( 623 $sTmp, 624 function ($val) { 625 return isset($val); 626 } 627 ); 628 629 $server = $sTmp; 630 } 631 632 /** 633 * Compare 2 normalized server arrays 634 * (Compares only the host and the port) 635 * 636 * @param array $serverA 637 * @param array $serverB 638 * @return int 639 */ 640 protected function compareServers(array $serverA, array $serverB) 641 { 642 $keyA = $serverA['host'] . ':' . $serverA['port']; 643 $keyB = $serverB['host'] . ':' . $serverB['port']; 644 if ($keyA === $keyB) { 645 return 0; 646 } 647 return $keyA > $keyB ? 1 : -1; 648 } 649} 650