<?php/** * memcached driver for PEAR::Cache using PEAR::Net_Socket * * This driver was made to provide a memcached connector without using * the pecl extension for PHP. You can use as many memcached servers as you want, * you just have to put them in the constructor options list like this: * * $options = array('servers' => array('server1:11211', 'server1:11211', 'server1:11211')); * * Timeout for dead server recheck can be set in $options['recheckTimeout']. * Default is 1 second. * Timeout for TCP connect can be set in $options['connectTimeout']. * Default is 0.05 seconds. * * PHP versions 4 and 5 * * LICENSE: This source file is subject to version 3.0 of the PHP license * that is available through the world-wide-web at the following URI: * http://www.php.net/license/3_0.txt. If you did not receive a copy of * the PHP License and are unable to obtain it through the web, please * send a note to license@php.net so we can mail you a copy immediately. * * @category Cache * @package Cache * @author Dieter Rothacker <dieter.rothacker@dmc.de> * @copyright 1997-2007 dmc digital media center GmbH * @license http://www.php.net/license/3_0.txt PHP License 3.0 * @version CVS: $Id: $ * @link http://pear.php.net/package/Cache * @see Cache/Container.php * @since File available since Release 1.5.6 */require_once 'Cache/Container.php';require_once 'Net/Socket.php';/** * memcached container driver class using PEAR::Net_Socket methods * * @category Cache * @package Cache * @author Dieter Rothacker <dieter.rothacker@dmc.de> * @copyright 1997-2007 dmc digital media center GmbH * @license http://www.php.net/license/3_0.txt PHP License 3.0 * @version 1.0.0 * @link http://pear.php.net/package/Cache */class Cache_Container_mcd extends Cache_Container{ /** * the classes version * * @access private * @var string $version */ var $version = '1.0.0'; /** * socket to establish connection * * @access private * @var handle $socket */ var $socket = null; /** * list of possible servers to connect to * * @access private * @var array $servers */ var $servers = array('localhost:11211'); /** * currently connected server number corresponding * to index-number in $servers * * @access private * @var string $currentServer */ var $currentServer = null; /** * blacklist for down servers * * @access private * @var array $blacklist */ var $blacklist = array(); /** * timeout in seconds to recheck blacklisted servers * default: 1 second * * @access private * @var float $recheckTimeout */ var $recheckTimeout = 1; /** * connect-timeout in seconds * default: 50ms * * @access private * @var float $connectTimeout */ var $connectTimeout = 0.05; /** * Options that can be set. Overriden from parent. * Lowwater + Highwater omitted on purpose because they have to be set * when initializing the memcached daemon * * @access private * @var array */ var $allowedOptions = array('encoding_mode', 'servers', 'recheckTimeout', 'connectTimeout'); /** * PHP4 constructor * * @param array $options array 'server' has to contain 'ip:port' elements * * @access public * @return void */ function Cache_Container_mcd($options) { /* register destructor */ register_shutdown_function(array(&$this, '__destruct')); /* call real constructor */ $this->__construct($options); } /* end func Cache_Container_mcd */ /** * constructor * * @param array $options array 'server' has to contain 'ip:port' elements * * @access public * @return mixed $returnValue null or Cache_Error */ function __construct($options) { if (is_array($options)) { $this->setOptions($options, $this->allowedOptions); } $this->socket = new Net_Socket(); } /* end func __construct */ /** * destructor * * @access public * @return void */ function __destruct() { $this->socket->disconnect(); } /* end func __destruct */ /** * generates new hashkey in case of down servers * * @param string $key old hashkey for cachedata * * @access private * @return string $key new hashkey for cachedata */ function getNewHashKey($key) { $returnValue = md5(serialize($key . 9)); return $returnValue; } /* end func getNewHashKey */ /** * establishes connection to server. * * @param integer $requestedServer server number in $servers * * @access private * @return mixed $returnValue true if connection was established, else false */ function establishConnection($requestedServer) { $returnValue = true; $sResult = null; $parts = array(); // if not already connected to requested server if ($this->currentServer != $requestedServer) { $this->currentServer = $requestedServer; $parts = explode(':', $this->servers[$requestedServer]); $server = $parts[0]; $port = $parts[1]; $sResult = $this->socket->connect($server, $port, false, $this->connectTimeout); } if (Net_Socket::isError($sResult)) { $returnValue = false; } return $returnValue; } /* end func establishConnection */ /** * checks if all servers are on blacklist (not connectable) * * @access private * @return string $returnValue true if all servers are down, else false */ function areAllServersDown() { $returnValue = false; if (count($this->blacklist) == count($this->servers)) { $returnValue = true; } return $returnValue; } /* end func areAllServersDown */ /** * returns new server number which is not blacklisted or returns false * - if all servers are on blacklist (all servers are not connectable) or * - if hashKey is not a md5 encoded string * * @param string $hashKey for cachedata * * @access private * @return String $returnValue serverNumber or false if all servers are on blacklist or hash is no md5 */ function getNewServerId($hashKey) { $returnValue = false; $decValue = false; $requestedServer = false; if (strlen($hashKey) == 32) { // compute new $requestedServer $decValue = substr($hashKey, 0, 7); $requestedServer = $decValue % count($this->servers); if ($this->areAllServersDown()) { $returnValue = false; } elseif ($this->isServerOnBlacklist($requestedServer)) { // try to connect to another server if current got blacklisted $returnValue = $this->getNewServerId($this->getNewHashKey($hashKey)); } else { if ($this->isServerRunning($requestedServer)) { $returnValue = $requestedServer; } else { // current server not reachable but not yet blacklisted $this->setServerToBlacklist($requestedServer); $returnValue = $this->getNewServerId($this->getNewHashKey($hashKey)); } } } else { $returnValue = new Cache_Error('getNewServerId: Hashkey is no valid md5', __FILE__, __LINE__); } return $returnValue; } /* end func getNewServerId */ /** * gets the version of the memacache server to check * if server is running. if connection to server can't be * established, server is set to blacklist. * * @param string $requestedServer number of server in order as in $server array * * @access private * @return string $returnValue if server is running true, else false */ function isServerRunning($requestedServer) { $returnValue = false; $isConnected = false; $sResult = false; $version = false; if ($requestedServer !== false) { $isConnected = $this->establishConnection($requestedServer); if ($isConnected) { $sResult = $this->socket->writeLine('version'); if (!Net_Socket::isError($sResult)) { $sResult = $this->socket->readLine(); if (substr($sResult, 0, 7) == 'VERSION') { $returnValue = true; } } } else { $this->setServerToBlacklist($requestedServer); } } return $returnValue; } /* end func IsServerRunning */ /** * Fetches a dataset from the storage medium. * * @param string $id dataset ID * @param string $group cache groups are not supported * * @return mixed $returnValue array, format: [expire date, cached data, user data] or Cache_Error * @access public */ function fetch($id, $group) { $userData = 0; $data = false; $sResult = false; $returnValue = array(); $this->freeBlacklistedServers(); $serverToFetch = $this->getNewServerId($id); if (!($serverToFetch === false)) { $sResult = $this->socket->writeLine('get ' . $id); if (Net_Socket::isError($sResult)) { $returnValue = new Cache_Error('Command "get": error -> Check your parameters', __FILE__, __LINE__); } else { $sResult = $this->socket->readLine(); if ($sResult == 'END' || $sResult == 'ERROR') { $returnValue = new Cache_Error('Command "get": no such key available', __FILE__, __LINE__); } else { $dataLine = explode(' ', $sResult); $userData = $dataLine[2]; $sResult = $this->socket->readLine(); $data = $this->decode($sResult); $returnValue = array($data[0], $data[1], $userData ); $this->socket->readLine(); } } } else { $returnValue = new Cache_Error('Command "get": all servers are unreachable', __FILE__, __LINE__); } return $returnValue; } /* end func fetch */ /** * Checks if a dataset exists * * @param string $id dataset ID * @param string $group cache group (not supported) * * @return boolean $returnValue true, if id exists else false * @access public */ function idExists($id, $group) { $returnValue = false; $returnValue = $this->fetch($id, $group); if (Net_Socket::isError($resultValue)) { $returnValue = false; } else { $returnValue = true; } return $returnValue; } /* end func idExists */ /** * removed cached data from server * * @param string $id dataset ID * @param string $group cache group * * @access private * @return boolean $returnValue true if removed */ function remove($id, $group) { $returnValue = true; $this->freeBlacklistedServers(); $serverToRemove = $this->getNewServerId($id); if (!($serverToRemove === false)) { $sResult = $this->socket->writeLine('delete ' . $id . ' 0'); $sResult = $this->socket->readLine(); if (Net_Socket::isError($sResult) || $sResult == 'NOT_FOUND') { $returnValue = new Cache_Error('Could not remove data', __FILE__, __LINE__); } } else { $returnValue = new Cache_Error('Could not remove data, all servers are down', __FILE__, __LINE__); } return $returnValue; } /* end func remove */ /** * Returns the userdata field of a cached data set. * * @param string $id dataset ID * @param string $group cache group * * @return mixed User_Data Field or Cache_Error * @access public */ function getUserdata($id, $group) { $resultValue = $this->fetch($id, null); if (!Net_Socket::isError($resultValue)) { $resultValue = $resultValue[2]; } return $resultValue; } /* end func getUserdata */ /** * Stores a dataset * * @param string $id dataset ID * @param mixed $data data to store * @param integer $expire expire date in seconds * @param string $group cache groups are not supported * @param string $userdata userdefined data * * @return mixed $returnValue Cache_Error or true if data was saved * @access public */ function save($id, $data, $expire, $group, $userdata) { $returnValue = false; $sResult = false; $userdata = (integer) $userdata; $this->freeBlacklistedServers(); if (is_null($id)) { $returnValue = new Cache_Error('Command "save": key error -> Key is needed to identify data', __FILE__, __LINE__); } if (!is_numeric($userdata) || ($userdata < 0) || ($userdata > pow(2, 16))) { $returnValue = new Cache_Error('Command "save": userdata has to be an unsigned 16bit int', __FILE__, __LINE__); } if (!is_numeric($expire) || ($expire < 0)) { $returnValue = new Cache_Error('Command "save": expire time has to be an unsigned int', __FILE__, __LINE__); } if (!is_a($returnValue, 'Cache_Error')) { /* we put expiration time into cache data because * memcache provides no functionality to read the * expiration time of an already stored cache item */ $cacheData = $this->encode(array( time() + $expire, $data,)); $serverToSave = $this->getNewServerId($id); if (!($serverToSave === false)) { $sResult = $this->socket->writeLine('set ' . $id . ' ' . $userdata . ' ' . $expire . ' ' . strlen($cacheData)); if (Net_Socket::isError($sResult)) { $returnValue = new Cache_Error('Command "save": command error -> Check your parameters', __FILE__, __LINE__); } else { $sResult = $this->socket->writeLine($cacheData); $sResult = $this->socket->readLine(); if ($sResult == 'STORED') { $returnValue = true; } } } else { $returnValue = new Cache_Error('Command "save": all servers are not reachable', __FILE__, __LINE__); } } return $returnValue; } /* end func save */ /** * Flushes the cache on all memcache servers * * @param string $group cache groups are not supported * * @return boolen $returnValue true if data was flushed, else false * @access public */ function flush($group) { $returnValue = false; $this->freeBlacklistedServers(); for ($i = 0; $i < count($this->servers); $i++) { $result = $this->establishConnection($i); if ($result === true) { $sResult = $this->socket->writeLine('flush_all'); $sResult = $this->socket->readLine(); if (!Net_Socket::isError($sResult)) { $returnValue = true; } } } return $returnValue; } /* end func flush */ /** * Checks if data is expired for future time. * if it is expired, memcache daemon removes it from memory automatically * (Hint: isExpired must be overriden, because memcache always returns actual time when * pear cache expects the value 0 for "no expiration time") * * @param string $id dataset ID * @param string $group cache groups are not supported * @param string $max_age maximum cache item age * * @return boolean $returnValue true if data is expired or not cached, else false * @access public */ function isExpired($id, $group, $max_age) { $returnValue = false; $fetchResult = $this->fetch($id, $group); if (!PEAR::isError($fetchResult)) { $expireDate = $fetchResult[0]; if ((time() + $max_age) <= $expireDate) { // is data expired in future point in time? (max_age) $returnValue = false; } elseif ($max_age === false) { // this means data is cached, no future-check (max_age) $returnValue = false; } else { // this means data is expired in future point in time (max_age) $returnValue = true; } } else { // data is expired because data couldn't be fetched $returnValue = true; } return $returnValue; } /* end func isExpired */ /** * Checks if a dataset is cached. * * @param string $id dataset ID * @param string $group cache groups are not supported * * @return boolean $returnValue true if data is cached, else false * @access public */ function isCached($id, $group) { $returnValue = $this->fetch($id, $group); if (Net_Socket::isError($returnValue)) { $returnValue = false; } else { $returnValue = true; } return $returnValue; } /* end func isCached */ /** * checks if $requestedServer is blacklisted * * @param integer $requestedServer server number * * @access private * @return boolean $returnValue true if server is blacklisted, else false */ function isServerOnBlacklist($requestedServer) { $returnValue = array_key_exists($requestedServer, $this->blacklist); return $returnValue; } /* end func isServerOnBlacklist */ /** * sets $requestedServer to blacklist. checks before blacklisting * if server is already on blacklist * * @param integer $requestedServer server number * * @access private * @return boolean $returnValue always true */ function setServerToBlacklist($requestedServer) { $returnValue = true; if (!$this->isServerOnBlacklist($requestedServer)) { $this->blacklist[$requestedServer] = time(); } return $returnValue; } /* end func setServerToBlacklist */ /** * Removes servers from blacklist if time has passed * * @access private * @return boolean $returnValue always true */ function freeBlacklistedServers() { $returnValue = true; $serverNo = false; $timestamp = false; foreach ($this->blacklist as $serverNo => $timestamp) { // free server if blacklisted for over $timeout if ((time() - $timestamp) > $this->recheckTimeout) { unset($this->blacklist[$serverNo]); } } return $returnValue; } /* end func freeBlacklistedServers */}?>