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 = $threshold['threshold'] ?? null; $minSavings = $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 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 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 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' => , 'weight' => , * 'timeout' => , 'retry_interval' => , * ) * * @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' => , 'port' => , '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 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' => , 'port' => , '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' => , 'port' => , 'weight' => , * 'status' => , 'persistent' => , * 'timeout' => , 'retry_interval' => , * ) * * @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([, [, [, [, [, [, ]]]]]]) $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; } }