torrentpier-lts/library/Zend/Cache/Storage/Adapter/Memcached.php

704 lines
20 KiB
PHP

<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 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 Memcached as MemcachedResource;
use stdClass;
use Traversable;
use Zend\Cache\Exception;
use Zend\Cache\Storage\AvailableSpaceCapableInterface;
use Zend\Cache\Storage\Capabilities;
use Zend\Cache\Storage\FlushableInterface;
use Zend\Cache\Storage\TotalSpaceCapableInterface;
class Memcached extends AbstractAdapter implements
AvailableSpaceCapableInterface,
FlushableInterface,
TotalSpaceCapableInterface
{
/**
* Major version of ext/memcached
*
* @var null|int
*/
protected static $extMemcachedMajorVersion;
/**
* Has this instance be initialized
*
* @var bool
*/
protected $initialized = false;
/**
* The memcached resource manager
*
* @var null|MemcachedResourceManager
*/
protected $resourceManager;
/**
* The memcached resource id
*
* @var null|string
*/
protected $resourceId;
/**
* The namespace prefix
*
* @var string
*/
protected $namespacePrefix = '';
/**
* Constructor
*
* @param null|array|Traversable|MemcachedOptions $options
* @throws Exception\ExceptionInterface
*/
public function __construct($options = null)
{
if (static::$extMemcachedMajorVersion === null) {
$v = (string) phpversion('memcached');
static::$extMemcachedMajorVersion = ($v !== '') ? (int) $v[0] : 0;
}
if (static::$extMemcachedMajorVersion < 1) {
throw new Exception\ExtensionNotLoadedException('Need ext/memcached version >= 1.0.0');
}
parent::__construct($options);
// reset initialized flag on update option(s)
$initialized = & $this->initialized;
$this->getEventManager()->attach('option', function () use (& $initialized) {
$initialized = false;
});
}
/**
* Initialize the internal memcached resource
*
* @return MemcachedResource
*/
protected function getMemcachedResource()
{
if (!$this->initialized) {
$options = $this->getOptions();
// get resource manager and resource id
$this->resourceManager = $options->getResourceManager();
$this->resourceId = $options->getResourceId();
// init namespace prefix
$namespace = $options->getNamespace();
if ($namespace !== '') {
$this->namespacePrefix = $namespace . $options->getNamespaceSeparator();
} else {
$this->namespacePrefix = '';
}
// update initialized flag
$this->initialized = true;
}
return $this->resourceManager->getResource($this->resourceId);
}
/* options */
/**
* Set options.
*
* @param array|Traversable|MemcachedOptions $options
* @return Memcached
* @see getOptions()
*/
public function setOptions($options)
{
if (!$options instanceof MemcachedOptions) {
$options = new MemcachedOptions($options);
}
return parent::setOptions($options);
}
/**
* Get options.
*
* @return MemcachedOptions
* @see setOptions()
*/
public function getOptions()
{
if (!$this->options) {
$this->setOptions(new MemcachedOptions());
}
return $this->options;
}
/* FlushableInterface */
/**
* Flush the whole storage
*
* @return bool
*/
public function flush()
{
$memc = $this->getMemcachedResource();
if (!$memc->flush()) {
throw $this->getExceptionByResultCode($memc->getResultCode());
}
return true;
}
/* TotalSpaceCapableInterface */
/**
* Get total space in bytes
*
* @return int|float
*/
public function getTotalSpace()
{
$memc = $this->getMemcachedResource();
$stats = $memc->getStats();
if ($stats === false) {
throw new Exception\RuntimeException($memc->getResultMessage());
}
$mem = array_pop($stats);
return $mem['limit_maxbytes'];
}
/* AvailableSpaceCapableInterface */
/**
* Get available space in bytes
*
* @return int|float
*/
public function getAvailableSpace()
{
$memc = $this->getMemcachedResource();
$stats = $memc->getStats();
if ($stats === false) {
throw new Exception\RuntimeException($memc->getResultMessage());
}
$mem = array_pop($stats);
return $mem['limit_maxbytes'] - $mem['bytes'];
}
/* reading */
/**
* Internal method to get an item.
*
* @param string $normalizedKey
* @param bool $success
* @param mixed $casToken
* @return mixed Data on success, null on failure
* @throws Exception\ExceptionInterface
*/
protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null)
{
$memc = $this->getMemcachedResource();
$internalKey = $this->namespacePrefix . $normalizedKey;
if (func_num_args() > 2) {
$result = $memc->get($internalKey, null, $casToken);
} else {
$result = $memc->get($internalKey);
}
$success = true;
if ($result === false) {
$rsCode = $memc->getResultCode();
if ($rsCode == MemcachedResource::RES_NOTFOUND) {
$result = null;
$success = false;
} elseif ($rsCode) {
$success = false;
throw $this->getExceptionByResultCode($rsCode);
}
}
return $result;
}
/**
* Internal method to get multiple items.
*
* @param array $normalizedKeys
* @return array Associative array of keys and values
* @throws Exception\ExceptionInterface
*/
protected function internalGetItems(array & $normalizedKeys)
{
$memc = $this->getMemcachedResource();
foreach ($normalizedKeys as & $normalizedKey) {
$normalizedKey = $this->namespacePrefix . $normalizedKey;
}
$result = $memc->getMulti($normalizedKeys);
if ($result === false) {
throw $this->getExceptionByResultCode($memc->getResultCode());
}
// remove namespace prefix from result
if ($result && $this->namespacePrefix !== '') {
$tmp = array();
$nsPrefixLength = strlen($this->namespacePrefix);
foreach ($result as $internalKey => & $value) {
$tmp[substr($internalKey, $nsPrefixLength)] = & $value;
}
$result = $tmp;
}
return $result;
}
/**
* Internal method to test if an item exists.
*
* @param string $normalizedKey
* @return bool
* @throws Exception\ExceptionInterface
*/
protected function internalHasItem(& $normalizedKey)
{
$memc = $this->getMemcachedResource();
$value = $memc->get($this->namespacePrefix . $normalizedKey);
if ($value === false) {
$rsCode = $memc->getResultCode();
if ($rsCode == MemcachedResource::RES_SUCCESS) {
return true;
} elseif ($rsCode == MemcachedResource::RES_NOTFOUND) {
return false;
} else {
throw $this->getExceptionByResultCode($rsCode);
}
}
return true;
}
/**
* Internal method to test multiple items.
*
* @param array $normalizedKeys
* @return array Array of found keys
* @throws Exception\ExceptionInterface
*/
protected function internalHasItems(array & $normalizedKeys)
{
$memc = $this->getMemcachedResource();
foreach ($normalizedKeys as & $normalizedKey) {
$normalizedKey = $this->namespacePrefix . $normalizedKey;
}
$result = $memc->getMulti($normalizedKeys);
if ($result === false) {
throw $this->getExceptionByResultCode($memc->getResultCode());
}
// Convert to a simgle list
$result = array_keys($result);
// remove namespace prefix
if ($result && $this->namespacePrefix !== '') {
$nsPrefixLength = strlen($this->namespacePrefix);
foreach ($result as & $internalKey) {
$internalKey = substr($internalKey, $nsPrefixLength);
}
}
return $result;
}
/**
* Get metadata of multiple items
*
* @param array $normalizedKeys
* @return array Associative array of keys and metadata
* @throws Exception\ExceptionInterface
*/
protected function internalGetMetadatas(array & $normalizedKeys)
{
$memc = $this->getMemcachedResource();
foreach ($normalizedKeys as & $normalizedKey) {
$normalizedKey = $this->namespacePrefix . $normalizedKey;
}
$result = $memc->getMulti($normalizedKeys);
if ($result === false) {
throw $this->getExceptionByResultCode($memc->getResultCode());
}
// remove namespace prefix and use an empty array as metadata
if ($this->namespacePrefix !== '') {
$tmp = array();
$nsPrefixLength = strlen($this->namespacePrefix);
foreach (array_keys($result) as $internalKey) {
$tmp[substr($internalKey, $nsPrefixLength)] = array();
}
$result = $tmp;
} else {
foreach ($result as & $value) {
$value = array();
}
}
return $result;
}
/* writing */
/**
* Internal method to store an item.
*
* @param string $normalizedKey
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
*/
protected function internalSetItem(& $normalizedKey, & $value)
{
$memc = $this->getMemcachedResource();
$expiration = $this->expirationTime();
if (!$memc->set($this->namespacePrefix . $normalizedKey, $value, $expiration)) {
throw $this->getExceptionByResultCode($memc->getResultCode());
}
return true;
}
/**
* Internal method to store multiple items.
*
* @param array $normalizedKeyValuePairs
* @return array Array of not stored keys
* @throws Exception\ExceptionInterface
*/
protected function internalSetItems(array & $normalizedKeyValuePairs)
{
$memc = $this->getMemcachedResource();
$expiration = $this->expirationTime();
$namespacedKeyValuePairs = array();
foreach ($normalizedKeyValuePairs as $normalizedKey => & $value) {
$namespacedKeyValuePairs[$this->namespacePrefix . $normalizedKey] = & $value;
}
if (!$memc->setMulti($namespacedKeyValuePairs, $expiration)) {
throw $this->getExceptionByResultCode($memc->getResultCode());
}
return array();
}
/**
* Add an item.
*
* @param string $normalizedKey
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
*/
protected function internalAddItem(& $normalizedKey, & $value)
{
$memc = $this->getMemcachedResource();
$expiration = $this->expirationTime();
if (!$memc->add($this->namespacePrefix . $normalizedKey, $value, $expiration)) {
if ($memc->getResultCode() == MemcachedResource::RES_NOTSTORED) {
return false;
}
throw $this->getExceptionByResultCode($memc->getResultCode());
}
return true;
}
/**
* Internal method to replace an existing item.
*
* @param string $normalizedKey
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
*/
protected function internalReplaceItem(& $normalizedKey, & $value)
{
$memc = $this->getMemcachedResource();
$expiration = $this->expirationTime();
if (!$memc->replace($this->namespacePrefix . $normalizedKey, $value, $expiration)) {
$rsCode = $memc->getResultCode();
if ($rsCode == MemcachedResource::RES_NOTSTORED) {
return false;
}
throw $this->getExceptionByResultCode($rsCode);
}
return true;
}
/**
* Internal method to set an item only if token matches
*
* @param mixed $token
* @param string $normalizedKey
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
* @see getItem()
* @see setItem()
*/
protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value)
{
$memc = $this->getMemcachedResource();
$expiration = $this->expirationTime();
$result = $memc->cas($token, $this->namespacePrefix . $normalizedKey, $value, $expiration);
if ($result === false) {
$rsCode = $memc->getResultCode();
if ($rsCode !== 0 && $rsCode != MemcachedResource::RES_DATA_EXISTS) {
throw $this->getExceptionByResultCode($rsCode);
}
}
return $result;
}
/**
* Internal method to remove an item.
*
* @param string $normalizedKey
* @return bool
* @throws Exception\ExceptionInterface
*/
protected function internalRemoveItem(& $normalizedKey)
{
$memc = $this->getMemcachedResource();
$result = $memc->delete($this->namespacePrefix . $normalizedKey);
if ($result === false) {
$rsCode = $memc->getResultCode();
if ($rsCode == MemcachedResource::RES_NOTFOUND) {
return false;
} elseif ($rsCode != MemcachedResource::RES_SUCCESS) {
throw $this->getExceptionByResultCode($rsCode);
}
}
return true;
}
/**
* Internal method to remove multiple items.
*
* @param array $normalizedKeys
* @return array Array of not removed keys
* @throws Exception\ExceptionInterface
*/
protected function internalRemoveItems(array & $normalizedKeys)
{
// support for removing multiple items at once has been added in ext/memcached-2.0.0
if (static::$extMemcachedMajorVersion < 2) {
return parent::internalRemoveItems($normalizedKeys);
}
$memc = $this->getMemcachedResource();
foreach ($normalizedKeys as & $normalizedKey) {
$normalizedKey = $this->namespacePrefix . $normalizedKey;
}
$rsCodes = $memc->deleteMulti($normalizedKeys);
$missingKeys = array();
foreach ($rsCodes as $key => $rsCode) {
if ($rsCode !== true && $rsCode != MemcachedResource::RES_SUCCESS) {
if ($rsCode != MemcachedResource::RES_NOTFOUND) {
throw $this->getExceptionByResultCode($rsCode);
}
$missingKeys[] = $key;
}
}
// remove namespace prefix
if ($missingKeys && $this->namespacePrefix !== '') {
$nsPrefixLength = strlen($this->namespacePrefix);
foreach ($missingKeys as & $missingKey) {
$missingKey = substr($missingKey, $nsPrefixLength);
}
}
return $missingKeys;
}
/**
* Internal method to increment an item.
*
* @param string $normalizedKey
* @param int $value
* @return int|bool The new value on success, false on failure
* @throws Exception\ExceptionInterface
*/
protected function internalIncrementItem(& $normalizedKey, & $value)
{
$memc = $this->getMemcachedResource();
$internalKey = $this->namespacePrefix . $normalizedKey;
$value = (int) $value;
$newValue = $memc->increment($internalKey, $value);
if ($newValue === false) {
$rsCode = $memc->getResultCode();
// initial value
if ($rsCode == MemcachedResource::RES_NOTFOUND) {
$newValue = $value;
$memc->add($internalKey, $newValue, $this->expirationTime());
$rsCode = $memc->getResultCode();
}
if ($rsCode) {
throw $this->getExceptionByResultCode($rsCode);
}
}
return $newValue;
}
/**
* Internal method to decrement an item.
*
* @param string $normalizedKey
* @param int $value
* @return int|bool The new value on success, false on failure
* @throws Exception\ExceptionInterface
*/
protected function internalDecrementItem(& $normalizedKey, & $value)
{
$memc = $this->getMemcachedResource();
$internalKey = $this->namespacePrefix . $normalizedKey;
$value = (int) $value;
$newValue = $memc->decrement($internalKey, $value);
if ($newValue === false) {
$rsCode = $memc->getResultCode();
// initial value
if ($rsCode == MemcachedResource::RES_NOTFOUND) {
$newValue = -$value;
$memc->add($internalKey, $newValue, $this->expirationTime());
$rsCode = $memc->getResultCode();
}
if ($rsCode) {
throw $this->getExceptionByResultCode($rsCode);
}
}
return $newValue;
}
/* status */
/**
* Internal method to get capabilities of this adapter
*
* @return Capabilities
*/
protected function internalGetCapabilities()
{
if ($this->capabilities === null) {
$this->capabilityMarker = new stdClass();
$this->capabilities = new Capabilities(
$this,
$this->capabilityMarker,
array(
'supportedDatatypes' => array(
'NULL' => true,
'boolean' => true,
'integer' => true,
'double' => true,
'string' => true,
'array' => true,
'object' => 'object',
'resource' => false,
),
'supportedMetadata' => array(),
'minTtl' => 1,
'maxTtl' => 0,
'staticTtl' => true,
'ttlPrecision' => 1,
'useRequestTime' => false,
'expiredRead' => false,
'maxKeyLength' => 255,
'namespaceIsPrefix' => true,
)
);
}
return $this->capabilities;
}
/* internal */
/**
* Get expiration time by ttl
*
* Some storage commands involve sending an expiration value (relative to
* an item or to an operation requested by the client) to the server. In
* all such cases, the actual value sent may either be Unix time (number of
* seconds since January 1, 1970, as an integer), or a number of seconds
* starting from current time. In the latter case, this number of seconds
* may not exceed 60*60*24*30 (number of seconds in 30 days); if the
* expiration value is larger than that, the server will consider it to be
* real Unix time value rather than an offset from current time.
*
* @return int
*/
protected function expirationTime()
{
$ttl = $this->getOptions()->getTtl();
if ($ttl > 2592000) {
return time() + $ttl;
}
return $ttl;
}
/**
* Generate exception based of memcached result code
*
* @param int $code
* @return Exception\RuntimeException
* @throws Exception\InvalidArgumentException On success code
*/
protected function getExceptionByResultCode($code)
{
switch ($code) {
case MemcachedResource::RES_SUCCESS:
throw new Exception\InvalidArgumentException(
"The result code '{$code}' (SUCCESS) isn't an error"
);
default:
return new Exception\RuntimeException($this->getMemcachedResource()->getResultMessage());
}
}
}