<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\Cache\Storage\Adapter; use ArrayAccess; use Memcache as MemcacheResource; use Traversable; use Zend\Cache\Exception; use Zend\Stdlib\ArrayUtils; /** * This is a resource manager for memcache */ class MemcacheResourceManager { /** * Registered resources * * @var array */ protected $resources = []; /** * Default server values per resource * * @var array */ protected $serverDefaults = []; /** * Failure callback per resource * * @var callable[] */ protected $failureCallbacks = []; /** * Check if a resource exists * * @param string $id * @return bool */ public function hasResource($id) { return isset($this->resources[$id]); } /** * Gets a memcache resource * * @param string $id * @return MemcacheResource * @throws Exception\RuntimeException */ public function getResource($id) { if (! $this->hasResource($id)) { throw new Exception\RuntimeException("No resource with id '{$id}'"); } $resource = $this->resources[$id]; if ($resource instanceof MemcacheResource) { return $resource; } $memc = new MemcacheResource(); $this->setResourceAutoCompressThreshold( $memc, $resource['auto_compress_threshold'], $resource['auto_compress_min_savings'] ); foreach ($resource['servers'] as $server) { $this->addServerToResource( $memc, $server, $this->serverDefaults[$id], $this->failureCallbacks[$id] ); } // buffer and return $this->resources[$id] = $memc; return $memc; } /** * Set a resource * * @param string $id * @param array|Traversable|MemcacheResource $resource * @param callable $failureCallback * @param array|Traversable $serverDefaults * @return MemcacheResourceManager Provides a fluent interface */ public function setResource($id, $resource, $failureCallback = null, $serverDefaults = []) { $id = (string) $id; if ($serverDefaults instanceof Traversable) { $serverDefaults = ArrayUtils::iteratorToArray($serverDefaults); } elseif (! is_array($serverDefaults)) { throw new Exception\InvalidArgumentException( 'ServerDefaults must be an instance Traversable or an array' ); } if (! ($resource instanceof MemcacheResource)) { if ($resource instanceof Traversable) { $resource = ArrayUtils::iteratorToArray($resource); } elseif (! is_array($resource)) { throw new Exception\InvalidArgumentException( 'Resource must be an instance of Memcache or an array or Traversable' ); } if (isset($resource['server_defaults'])) { $serverDefaults = array_merge($serverDefaults, $resource['server_defaults']); unset($resource['server_defaults']); } $resourceOptions = [ 'servers' => [], 'auto_compress_threshold' => null, 'auto_compress_min_savings' => null, ]; $resource = array_merge($resourceOptions, $resource); // normalize and validate params $this->normalizeAutoCompressThreshold( $resource['auto_compress_threshold'], $resource['auto_compress_min_savings'] ); $this->normalizeServers($resource['servers']); } $this->normalizeServerDefaults($serverDefaults); $this->resources[$id] = $resource; $this->failureCallbacks[$id] = $failureCallback; $this->serverDefaults[$id] = $serverDefaults; return $this; } /** * Remove a resource * * @param string $id * @return MemcacheResourceManager Provides a fluent interface */ public function removeResource($id) { unset($this->resources[$id]); return $this; } /** * Normalize compress threshold options * * @param int|string|array|ArrayAccess $threshold * @param float|string $minSavings */ protected function normalizeAutoCompressThreshold(& $threshold, & $minSavings) { if (is_array($threshold) || ($threshold instanceof ArrayAccess)) { $tmpThreshold = (isset($threshold['threshold'])) ? $threshold['threshold'] : null; $minSavings = (isset($threshold['min_savings'])) ? $threshold['min_savings'] : $minSavings; $threshold = $tmpThreshold; } if (isset($threshold)) { $threshold = (int) $threshold; } if (isset($minSavings)) { $minSavings = (float) $minSavings; } } /** * Set compress threshold on a Memcache resource * * @param MemcacheResource $resource * @param int $threshold * @param float $minSavings */ protected function setResourceAutoCompressThreshold(MemcacheResource $resource, $threshold, $minSavings) { if (! isset($threshold)) { return; } if (isset($minSavings)) { $resource->setCompressThreshold($threshold, $minSavings); } else { $resource->setCompressThreshold($threshold); } } /** * Get compress threshold * * @param string $id * @return int|null * @throws \Zend\Cache\Exception\RuntimeException */ public function getAutoCompressThreshold($id) { if (! $this->hasResource($id)) { throw new Exception\RuntimeException("No resource with id '{$id}'"); } $resource = & $this->resources[$id]; if ($resource instanceof MemcacheResource) { // Cannot get options from Memcache resource once created throw new Exception\RuntimeException("Cannot get compress threshold once resource is created"); } return $resource['auto_compress_threshold']; } /** * Set compress threshold * * @param string $id * @param int|string|array|ArrayAccess|null $threshold * @param float|string|bool $minSavings * @return MemcacheResourceManager Provides a fluent interface */ public function setAutoCompressThreshold($id, $threshold, $minSavings = false) { if (! $this->hasResource($id)) { return $this->setResource($id, [ 'auto_compress_threshold' => $threshold, ]); } $this->normalizeAutoCompressThreshold($threshold, $minSavings); $resource = & $this->resources[$id]; if ($resource instanceof MemcacheResource) { $this->setResourceAutoCompressThreshold($resource, $threshold, $minSavings); } else { $resource['auto_compress_threshold'] = $threshold; if ($minSavings !== false) { $resource['auto_compress_min_savings'] = $minSavings; } } return $this; } /** * Get compress min savings * * @param string $id * @return float|null * @throws Exception\RuntimeException */ public function getAutoCompressMinSavings($id) { if (! $this->hasResource($id)) { throw new Exception\RuntimeException("No resource with id '{$id}'"); } $resource = & $this->resources[$id]; if ($resource instanceof MemcacheResource) { // Cannot get options from Memcache resource once created throw new Exception\RuntimeException("Cannot get compress min savings once resource is created"); } return $resource['auto_compress_min_savings']; } /** * Set compress min savings * * @param string $id * @param float|string|null $minSavings * @return MemcacheResourceManager Provides a fluent interface * @throws \Zend\Cache\Exception\RuntimeException */ public function setAutoCompressMinSavings($id, $minSavings) { if (! $this->hasResource($id)) { return $this->setResource($id, [ 'auto_compress_min_savings' => $minSavings, ]); } $minSavings = (float) $minSavings; $resource = & $this->resources[$id]; if ($resource instanceof MemcacheResource) { throw new Exception\RuntimeException( "Cannot set compress min savings without a threshold value once a resource is created" ); } else { $resource['auto_compress_min_savings'] = $minSavings; } return $this; } /** * Set default server values * array( * 'persistent' => <persistent>, 'weight' => <weight>, * 'timeout' => <timeout>, 'retry_interval' => <retryInterval>, * ) * @param string $id * @param array $serverDefaults * @return MemcacheResourceManager Provides a fluent interface */ public function setServerDefaults($id, array $serverDefaults) { if (! $this->hasResource($id)) { return $this->setResource($id, [ 'server_defaults' => $serverDefaults ]); } $this->normalizeServerDefaults($serverDefaults); $this->serverDefaults[$id] = $serverDefaults; return $this; } /** * Get default server values * * @param string $id * @return array * @throws Exception\RuntimeException */ public function getServerDefaults($id) { if (! isset($this->serverDefaults[$id])) { throw new Exception\RuntimeException("No resource with id '{$id}'"); } return $this->serverDefaults[$id]; } /** * @param array $serverDefaults * @throws Exception\InvalidArgumentException */ protected function normalizeServerDefaults(& $serverDefaults) { if (! is_array($serverDefaults) && ! ($serverDefaults instanceof Traversable)) { throw new Exception\InvalidArgumentException( "Server defaults must be an array or an instance of Traversable" ); } // Defaults $result = [ 'persistent' => true, 'weight' => 1, 'timeout' => 1, // seconds 'retry_interval' => 15, // seconds ]; foreach ($serverDefaults as $key => $value) { switch ($key) { case 'persistent': $value = (bool) $value; break; case 'weight': case 'timeout': case 'retry_interval': $value = (int) $value; break; } $result[$key] = $value; } $serverDefaults = $result; } /** * Set callback for server connection failures * * @param string $id * @param callable|null $failureCallback * @return MemcacheResourceManager Provides a fluent interface */ public function setFailureCallback($id, $failureCallback) { if (! $this->hasResource($id)) { return $this->setResource($id, [], $failureCallback); } $this->failureCallbacks[$id] = $failureCallback; return $this; } /** * Get callback for server connection failures * * @param string $id * @return callable * @throws Exception\RuntimeException */ public function getFailureCallback($id) { if (! isset($this->failureCallbacks[$id])) { throw new Exception\RuntimeException("No resource with id '{$id}'"); } return $this->failureCallbacks[$id]; } /** * Get servers * * @param string $id * @throws Exception\RuntimeException * @return array array('host' => <host>, 'port' => <port>, 'weight' => <weight>) */ public function getServers($id) { if (! $this->hasResource($id)) { throw new Exception\RuntimeException("No resource with id '{$id}'"); } $resource = & $this->resources[$id]; if ($resource instanceof MemcacheResource) { throw new Exception\RuntimeException("Cannot get server list once resource is created"); } return $resource['servers']; } /** * Add servers * * @param string $id * @param string|array $servers * @return MemcacheResourceManager Provides a fluent interface */ public function addServers($id, $servers) { if (! $this->hasResource($id)) { return $this->setResource($id, [ 'servers' => $servers ]); } $this->normalizeServers($servers); $resource = & $this->resources[$id]; if ($resource instanceof MemcacheResource) { foreach ($servers as $server) { $this->addServerToResource( $resource, $server, $this->serverDefaults[$id], $this->failureCallbacks[$id] ); } } else { // don't add servers twice $resource['servers'] = array_merge( $resource['servers'], array_udiff($servers, $resource['servers'], [$this, 'compareServers']) ); } return $this; } /** * Add one server * * @param string $id * @param string|array $server * @return MemcacheResourceManager */ public function addServer($id, $server) { return $this->addServers($id, [$server]); } /** * @param MemcacheResource $resource * @param array $server * @param array $serverDefaults * @param callable|null $failureCallback */ protected function addServerToResource( MemcacheResource $resource, array $server, array $serverDefaults, $failureCallback ) { // Apply server defaults $server = array_merge($serverDefaults, $server); // Reorder parameters $params = [ $server['host'], $server['port'], $server['persistent'], $server['weight'], $server['timeout'], $server['retry_interval'], $server['status'], ]; if (isset($failureCallback)) { $params[] = $failureCallback; } call_user_func_array([$resource, 'addServer'], $params); } /** * Normalize a list of servers into the following format: * array(array('host' => <host>, 'port' => <port>, 'weight' => <weight>)[, ...]) * * @param string|array $servers */ protected function normalizeServers(& $servers) { if (is_string($servers)) { // Convert string into a list of servers $servers = explode(',', $servers); } $result = []; foreach ($servers as $server) { $this->normalizeServer($server); $result[$server['host'] . ':' . $server['port']] = $server; } $servers = array_values($result); } /** * Normalize one server into the following format: * array( * 'host' => <host>, 'port' => <port>, 'weight' => <weight>, * 'status' => <status>, 'persistent' => <persistent>, * 'timeout' => <timeout>, 'retry_interval' => <retryInterval>, * ) * * @param string|array $server * @throws Exception\InvalidArgumentException */ protected function normalizeServer(& $server) { // WARNING: The order of this array is important. // Used for converting an ordered array to a keyed array. // Append new options, do not insert or you will break BC. $sTmp = [ 'host' => null, 'port' => 11211, 'weight' => null, 'status' => true, 'persistent' => null, 'timeout' => null, 'retry_interval' => null, ]; // convert a single server into an array if ($server instanceof Traversable) { $server = ArrayUtils::iteratorToArray($server); } if (is_array($server)) { if (isset($server[0])) { // Convert ordered array to keyed array // array(<host>[, <port>[, <weight>[, <status>[, <persistent>[, <timeout>[, <retryInterval>]]]]]]) $server = array_combine( array_slice(array_keys($sTmp), 0, count($server)), $server ); } $sTmp = array_merge($sTmp, $server); } elseif (is_string($server)) { // parse server from URI host{:?port}{?weight} $server = trim($server); if (strpos($server, '://') === false) { $server = 'tcp://' . $server; } $urlParts = parse_url($server); if (! $urlParts) { throw new Exception\InvalidArgumentException("Invalid server given"); } $sTmp = array_merge($sTmp, array_intersect_key($urlParts, $sTmp)); if (isset($urlParts['query'])) { $query = null; parse_str($urlParts['query'], $query); $sTmp = array_merge($sTmp, array_intersect_key($query, $sTmp)); } } if (! $sTmp['host']) { throw new Exception\InvalidArgumentException('Missing required server host'); } // Filter values foreach ($sTmp as $key => $value) { if (isset($value)) { switch ($key) { case 'host': $value = (string) $value; break; case 'status': case 'persistent': $value = (bool) $value; break; case 'port': case 'weight': case 'timeout': case 'retry_interval': $value = (int) $value; break; } } $sTmp[$key] = $value; } $sTmp = array_filter( $sTmp, function ($val) { return isset($val); } ); $server = $sTmp; } /** * Compare 2 normalized server arrays * (Compares only the host and the port) * * @param array $serverA * @param array $serverB * @return int */ protected function compareServers(array $serverA, array $serverB) { $keyA = $serverA['host'] . ':' . $serverA['port']; $keyB = $serverB['host'] . ':' . $serverB['port']; if ($keyA === $keyB) { return 0; } return $keyA > $keyB ? 1 : -1; } }