Arquivos
hhvm/hphp/system/php/redis/Redis.php
T
Sean Cannella 22c5361a24 redis fixes from github comments
- applying github comment fixes (thanks @staabm )
2013-06-27 13:00:37 -07:00

1557 linhas
48 KiB
PHP

<?php
class Redis {
/* Redis servers run here by default */
const DEFAULT_PORT = 6379;
/* Return values from Redis::type() */
const REDIS_NOT_FOUND = 0;
const REDIS_STRING = 1;
const REDIS_SET = 2;
const REDIS_LIST = 3;
const REDIS_ZSET = 4;
const REDIS_HASH = 5;
/* Operational modes
*
* In ATOMIC mode, we wait for the server to
* respond to each request and return the value
* directly.
*
* In MULTI mode (activated by calling Redis::multi())
* we send commands immediately. but they are held in
* a transaction by the server. Only upon calling
* Redis::exec() is the transaction committed, and the
* results returned.
*
* In PIPELINE mode (activated by calling Redis::pipeline())
* we queue all commands locally until invoking Redis::exec()
* at which point they are sent to the server in a single batch.
* And all results are packaged back in a single batch.
*
* In both MULTI and PIPELINE modes, pending commands may be
* discarded by calling Redis::discard()
* The return value for both MULTI and PIPELINE for most commands
* is the object itself, meaning fluent calling may be used.
*/
const ATOMIC = 0;
const MULTI = 1;
const PIPELINE = 2;
/* Options to Redis::setOption() and Redis::getOption() */
const OPT_SERIALIZER = 1;
const OPT_PREFIX = 2;
const OPT_READ_TIMEOUT = 3;
/* Type of serialization to use with values stored in redis */
const SERIALIZER_NONE = 0;
const SERIALIZER_PHP = 1;
const SERIALIZER_IGBINARY = 2;
/* Options used by lInsert and similar methods */
const AFTER = 'after';
const BEFORE = 'before';
/* Connection ---------------------------------------------------------- */
public function connect($host,
$port = -1,
$timeout = 0.0,
$persistent_id = '',
$retry_interval = 0) {
return $this->doConnect($host, $port, $timeout, $persistent_id,
$retry_interval, false);
}
public function pconnect($host,
$port = -1,
$timeout = 0.0,
$persistent_id = '',
$retry_interval = 0) {
return $this->doConnect($host, $port, $timeout, $persistent_id,
$retry_interval, true);
}
public function auth($password) {
$this->password = $password;
$this->processCommand('AUTH', $password);
return $this->processBooleanResponse();
}
public function close() {
$this->processCommand('QUIT');
fclose($this->connection);
$this->connection = null;
}
public function select($dbNumber) {
$this->dbNumber = (int)$dbNumber;
$this->processCommand("SELECT", (int)$dbNumber);
return $this->processBooleanResponse();
}
public function setOption($opt, $value) {
switch ($opt) {
case self::OPT_PREFIX:
$this->prefix = $value;
return true;
case self::OPT_SERIALIZER:
if (($value !== self::SERIALIZER_NONE) AND
($value !== self::SERIALIZER_PHP) AND
($value !== self::SERIALIZER_IGBINARY)) {
throw new RedisException("Invalid serializer option: $value");
}
$this->serializer = (int)$value;
return true;
case self::OPT_READ_TIMEOUT:
$this->timeout_seconds = (int)($value - ((int)$value));
$this->timeout_useconds = (int)(($value - $seconds) * 1000000);
return stream_set_timeout($this->connection, $this->timeout_seconds,
$this->timeout_useconds);
default:
return false;
}
}
public function getOption($opt) {
switch ($opt) {
case self::OPT_PREFIX: return $this->prefix;
case self::OPT_SERIALIZER: return $this->serializer;
case self::OPT_READ_TIMEOUT:
return $this->timeout_seconds + ($this->timeout_useconds / 1000000);
}
return false;
}
/* Server -------------------------------------------------------------- */
public function config($op, $key, $val = '') {
if ($op == 'GET') {
$this->processCommand('CONFIG', 'GET', $key);
return $this->processMapResponse(false, false);
} else if ($op == 'SET') {
$this->processCommand('CONFIG', 'SET', $key, $val);
return $this->processBooleanResponse();
} else {
throw new RedisException('First arg must be GET or SET');
}
}
public function info($option = '') {
if ($option) {
$this->processCommand('INFO', $option);
} else {
$this->processCommand('INFO');
}
return $this->processInfoResponse();
}
public function resetStat() {
$this->processCommand('CONFIG', 'RESETSTAT');
return $this->processBooleanResponse();
}
public function slaveOf($host = '', $port = -1) {
if ($host) {
if ($port <= 0) {
$port = self::DEFAULT_PORT;
}
$this->processCommand('SLAVEOF', $host, (int)$port);
} else {
$this->processCommand('SLAVEOF', 'NO', 'ONE');
}
return $this->processBooleanResponse();
}
public function client($cmd, $arg = '') {
if (func_num_args() == 2) {
$this->processCommand('CLIENT', $cmd, $arg);
} else {
$this->processCommand('CLIENT', $cmd);
}
if ($cmd == 'list') {
return $this->processClientListResponse();
} else {
return $this->processVariantResponse();
}
}
/* Strings ------------------------------------------------------------- */
public function decr($key, $by = 1) {
if ($by !== 1) {
return $this->decrBy($key, $by);
}
$this->processCommand("DECR", $this->prefix($key));
return $this->processLongResponse();
}
public function decrBy($key, $by) {
if ($by === 1) {
return $this->decr($key);
}
$this->processCommand("DECRBY", $this->prefix($key), (int)$by);
return $this->processLongResponse();
}
public function incr($key, $by = 1) {
if ($by !== 1) {
return $this->incrBy($key, $by);
}
$this->processCommand("INCR", $this->prefix($key));
return $this->processLongResponse();
}
public function incrBy($key, $by) {
if ($by === 1) {
return $this->incr($key);
}
$this->processCommand("INCRBY", $this->prefix($key), (int)$by);
return $this->processLongResponse();
}
public function incrByFloat($key, $by) {
$this->processCommand("INCRBYFLOAT", $this->prefix($key),
(float)$by);
return $this->processDoubleResponse();
}
public function set($key, $value, $expire = -1) {
$key = $this->prefix($key);
$value = $this->serialize($value);
if ($expire > 0) {
$this->processCommand("SETEX", $key, $expire, $value);
} else {
$this->processCommand("SET", $key, $value);
}
return $this->processBooleanResponse();
}
/* Keys ---------------------------------------------------------------- */
public function sort($key, array $arr = null) {
$args = $this->sortClause($arr, $using_store);
array_unshift($args, $key);
$this->processArrayCommand('SORT', $args);
if ($using_store) {
return $this->processVectorResponse(true);
} else {
return $this->processLongResponse();
}
}
public function sortAsc($key,
$pattern = null,
$get = null,
$start = -1,
$count = -1,
$store = null) {
$limit = (($start > 0) AND ($count > 0)) ? [$start, $count] : null;
return $this->sort($key, [
'by' => $pattern,
'get' => $get,
'limit' => $limit,
'store' => $store,
'dir' => 'ASC',
]);
}
public function sortAscAlpha($key,
$pattern = null,
$get = null,
$start = -1,
$count = -1,
$store = null) {
$limit = (($start > 0) AND ($count > 0)) ? [$start, $count] : null;
return $this->sort($key, [
'by' => $pattern,
'get' => $get,
'limit' => $limit,
'store' => $store,
'dir' => 'ASC',
'alpha' => true,
]);
}
public function sortDesc($key,
$pattern = null,
$get = null,
$start = -1,
$count = -1,
$store = null) {
$limit = (($start > 0) AND ($count > 0)) ? [$start, $count] : null;
return $this->sort($key, [
'by' => $pattern,
'get' => $get,
'limit' => $limit,
'store' => $store,
'dir' => 'DESC',
]);
}
public function sortDescAlpha($key,
$pattern = null,
$get = null,
$start = -1,
$count = -1,
$store = null) {
$limit = (($start > 0) AND ($count > 0)) ? [$start, $count] : null;
return $this->sort($key, [
'by' => $pattern,
'get' => $get,
'limit' => $limit,
'store' => $store,
'dir' => 'DESC',
'alpha' => true,
]);
}
public function object($info, $key) {
$this->processCommand('OBJECT', $info, $this->prefix($key));
switch ($info) {
case 'refcount': return $this->processLongResponse();
case 'encoding': return $this->processStringResponse();
default: return $this->processBooleanResponse();
}
}
/* Hashes -------------------------------------------------------------- */
public function hMGet($key, array $members) {
$members = array_values($members);
$args = [$this->prefix($key)] + $members;
$this->processArrayCommand('HMGET', $args);
return $this->processAssocResponse($members);
}
public function hMSet($key, array $pairs) {
$args = [$this->prefix($key)];
foreach ($pairs as $k => $v) {
$args[] = $k;
$args[] = $this->serialize($v);
}
$this->processArrayCommand('HMSET', $args);
return $this->processBooleanResponse();
}
/* zSets --------------------------------------------------------------- */
public function zAdd($key, $score, $value/*, $scoreN, $valueN */) {
$args = func_get_args();
$count = count($args);
if (($count - 1) % 2) {
return false;
}
$args[0] = $this->prefix($args[0]);
for ($i = 1; $i < $count; $i += 2) {
$args[$i ] = (double)$args[$i];
$args[$i+1] = $this->serialize($args[$i+1]);
}
$this->processArrayCommand('ZADD', $args);
return $this->processLongResponse();
}
protected function zInterUnionStore($cmd,
$key,
array $keys,
array $weights = null,
$op = '') {
$args = [ $this->prefix($key), count($keys) ];
foreach ($keys as $k) {
$args[] = $this->prefix($k);
}
if ($weights) {
$args[] = 'WEIGHTS';
foreach ($weights as $weight) {
if (is_int($weight) OR
is_float($weight) OR
($weight === 'inf') OR
($weight === '-inf') OR
($weight === '+inf')) {
$args[] = $weight;
}
}
}
if ($op) {
$args[] = 'AGGREGATE';
$args[] = $op;
}
$this->processArrayCommand($cmd, $args);
return $this->processLongResponse();
}
public function zInterStore($key,
array $keys,
array $weights = null,
$op = '') {
return $this->zInterUnionStore('ZINTERSTORE', $key, $keys, $weights, $op);
}
public function zUnionStore($key,
array $keys,
array $weights = null,
$op = '') {
return $this->zInterUnionStore('ZUNIONSTORE', $key, $keys, $weights, $op);
}
public function zRange($key, $start, $end, $withscores = false) {
$args = [
$this->prefix($key),
(int)$start,
(int)$end,
];
if ($withscores) {
$args[] = 'WITHSCORES';
}
$this->processCommand('ZRANGE', $args);
if ($withscores) {
return $this->processMapResponse(true, false);
} else {
return $this->processVectorResponse(true);
}
}
protected function zRangeByScoreImpl($cmd,
$key,
$start,
$end,
array $opts = null) {
$args = [$this->prefix($key), (int)$start, (int)$end];
if (isset($opts['limit']) AND
is_array($opts['limit']) AND
(count($opts['limit']) == 2)) {
list($limit_start, $limit_end) = $opts['limit'];
$args[] = 'LIMIT';
$args[] = $limit_start;
$args[] = $limit_end;
}
if (!empty($opts['withscores'])) {
$args[] = 'WITHSCORES';
}
$this->processArrayCommand($cmd, $args);
if (!empty($opts['withscores'])) {
return $this->processMapResponse(true, false);
} else {
return $this->processVectorResponse(true);
}
}
public function zRangeByScore($key, $start, $end, array $opts = null) {
return $this->zRangeByScoreImpl('ZRANGEBYSCORE',
$key, $start, $end, $opts);
}
public function zRevRangeByScore($key, $start, $end, array $opts = null) {
return $this->zRangeByScoreImpl('ZREVRANGEBYSCORE',
$key, $start, $end, $opts);
}
public function zRevRange($key, $start, $end, $withscore = false) {
$args = [
$this->prefix($key),
(int)$start,
(int)$end,
];
if ($withscores) {
$args[] = 'WITHSCORES';
}
$this->processCommand('ZREVRANGE', $args);
if ($withscores) {
return $this->processMapResponse(true, false);
} else {
return $this->processVectorResponse(true);
}
}
/* Multi --------------------------------------------------------------- */
protected function flushCallbacks($multibulk = true) {
if ($multibulk) $this->sockReadData($type); // Response Count
$ret = [];
foreach ($this->multiHandler as $callback) {
$args = isset($callback['args']) ? $callback['args'] : [];
$ret[] = call_user_func_array($callback['cb'], $args);
}
$this->multiHandler = [];
return $ret;
}
public function multi($mode = self::MULTI) {
if ($mode === self::PIPELINE) {
return $this->pipeline();
}
if ($mode !== self::MULTI) {
return false;
}
$this->discard();
$this->processCommand('MULTI');
$resp = $this->sockReadData($type);
if (($type === self::TYPE_LINE) AND ($resp === 'OK')) {
$this->mode = self::MULTI;
return $this;
}
return false;
}
public function exec() {
if ($this->mode === self::MULTI) {
$this->mode = self::ATOMIC;
$this->processCommand('EXEC');
return $this->flushCallbacks();
} else if ($this->mode === self::PIPELINE) {
$this->mode = self::ATOMIC;
foreach ($this->commands as $cmd) {
$this->processArrayCommand($cmd['cmd'], $cmd['args']);
}
$this->commands = [];
return $this->flushCallbacks(false);
}
}
public function discard() {
$discard = ($this->mode === self::MULTI);
$this->mode = self::ATOMIC;
$this->commands = [];
$this->multiHandler = [];
if ($discard) {
$this->processCommand('DISCARD');
return $this->process1Response();
}
return true;
}
public function pipeline() {
$this->discard();
$this->mode = self::PIPELINE;
return $this;
}
/* Batch --------------------------------------------------------------- */
protected function processMSetCommand($cmd, array $data) {
$args = [];
foreach ($data as $key => $val) {
$args[] = $this->prefix($key);
$args[] = $this->serialize($val);
}
$this->processArrayCommand($cmd, $args);
}
public function mSet(array $data) {
$this->processMSetCommand('MSET', $data);
return $this->processBooleanResponse();
}
public function mSetNx(array $data) {
$this->processMSetCommand('MSETNX', $data);
return $this->process1Response();
}
/* Scripting ----------------------------------------------------------- */
protected function doEval($cmd, $script, array $args, $numKeys) {
foreach($args as &$arg) {
if ($numKeys-- <= 0) break;
$arg = $this->prefix($arg);
}
array_unshift($args, $script);
$this->processArrayCommand($cmd, $args);
return $this->processVariantResponse();
}
public function _eval($script, array $args = null, $numKeys = 0) {
return $this->doEval('EVAL', $script, $args, $numKeys);
}
public function evalSha($sha, array $args = null, $numKeys = 0) {
return $this->doEval('EVALSHA', $sha, $args, $numKeys);
}
public function script($subcmd/* ... */) {
switch ($subcmd) {
case 'flush':
case 'kill':
$this->processCommand('SCRIPT', $subcmd);
return $this->processVariantResponse();
case 'load':
if (func_num_args() < 2) {
return false;
}
$script = func_get_arg(1);
if (!is_string($script) OR empty($script)) {
return false;
}
$this->processCommand('SCRIPT', 'load', $script);
return $this->processVariantResponse();
case 'exists':
$args = func_get_args();
$args[0] = 'EXISTS';
$this->processArrayCommand('SCRIPT', $args);
return $this->processVariantResponse();
default:
return false;
}
}
/* Introspection ------------------------------------------------------- */
public function isConnected() {
return $this->checkConnection(false);
}
public function getHost() {
return $this->host;
}
public function getPort() {
return $this->port;
}
public function getDBNum() {
return $this->dbNumber;
}
public function getTimeout() {
return $this->getReadTimeout();
}
public function getReadTimeout() {
return $this->getOption(self::OPT_READ_TIMEOUT);
}
public function getPersistentId() {
throw new RedisException('Named persistent connections are '.
'not supported.');
}
public function getPassword() {
return $this->password;
}
public function getLastError() {
return $this->lastError;
}
public function clearLastError() {
$this->lastError = '';
return true;
}
/* Standard Function Map ----------------------------------------------- */
/**
* The majority of the Redis API is implemnted by __call
* which references this list for how the individual command
* should be handled.
*
* By default the name of the method (key in this array)
* is uppercased to become the actual command sent to the redis server.
*
* Example: 'get' becomes the Redis command `GET`
*
* This mapping may be overridden by adding a 'cmd' element such as:
* 'myget' => [ 'cmd' => 'GET' ],
*
* The argument spec is defined by the 'format' subparameter with each
* position in the string specifying what type of param it is.
* 's' => Ordinary string to be passed trough to the server unmodified
* 'k' => The name of a key. Prefixed with $this->prefix.
* 'v' => A piece of user data. Serialized according to $this->serialize.
* 'l' => An integer(long). Explicitly cast from whatever is passed.
* 'd' => A float(double). Explicitly cast from whatever is passed.
* 'b' => A boolean. Explicitly cast from whatever is passed.
* 'p' => Pivot point (self::BEFORE or self::AFTER). Validated.
*
* In lieu of 'format', a mapping may specify 'vararg' for variadic methods.
* The value must be a bitmask of the VAR_* constants.
* See Redis::translateVarArgs()
*
* The method (on this class) called to handle the response is named by the
* 'handler' field. A shortcut for this is the 'return' field which will
* be mapped into 'handler' as: 'process{$return}Response'
* To pass arguments to the handler, use 'retargs'.
*
* Lastly, the 'alias' field (given by itself), will map calls from one
* function directly to another. If the target method actually exists,
* the fcall will be proxied through call_user_func_array(). If the target
* is elsewhere in the map, __call's state will be reset to use the new
* map element.
*/
protected static $map = [
// Connection
'open' => [ 'alias' => 'connect' ],
'popen' => [ 'alias' => 'pconnect' ],
'ping' => [ 'return' => 'Raw' ],
'echo' => [ 'format' => 's', 'return' => 'String' ],
// Server
'bgrewriteaof' => [ 'return' => 'Boolean' ],
'bgsave' => [ 'return' => 'Boolean' ],
'dbsize' => [ 'return' => 'Long' ],
'flushall' => [ 'return' => 'Boolean' ],
'flushdb' => [ 'return' => 'Boolean' ],
'lastsave' => [ 'return' => 'Long' ],
'save' => [ 'return' => 'Boolean' ],
'time' => [ 'return' => 'Vector' ],
// Strings
'append' => [ 'format' => 'kv', 'return' => 'Long' ],
'bitcount' => [ 'format' => 'kll', 'return' => 'Long' ],
'bitop' => [ 'vararg' => self::VAR_KEY_NOT_FIRST, 'return' => 'Long' ],
'get' => [ 'format' => 'k', 'return' => 'Serialized' ],
'getbit' => [ 'format' => 'kl', 'return' => 'Long' ],
'getrange' => [ 'format' => 'kll', 'return' => 'String', 'cmd' => 'RANGE' ],
'getset' => [ 'format' => 'kv', 'return' => 'Serialized' ],
'setbit' => [ 'format' => 'klv', 'return' => 'Long' ],
'setex' => [ 'format' => 'klv', 'return' => 'Boolean' ],
'psetex' => [ 'format' => 'klv', 'return' => 'Boolean' ],
'setnx' => [ 'format' => 'kv', 'return' => '1' ],
'setrange' => [ 'format' => 'klv', 'return' => 'Long' ],
'strlen' => [ 'format' => 'k', 'return' => 'Long' ],
// Keys
'del' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Long' ],
'delete' => [ 'alias' => 'del' ],
'dump' => [ 'format' => 'k', 'return' => 'Raw' ],
'exists' => [ 'format' => 'k', 'return' => '1' ],
'expire' => [ 'format' => 'kl', 'return' => '1' ],
'settimeout' => [ 'alias' => 'expire' ],
'pexpire' => [ 'format' => 'kl', 'return' => '1' ],
'expireat' => [ 'format' => 'kl', 'return' => '1' ],
'pexpireat' => [ 'format' => 'kl', 'return' => '1' ],
'keys' => [ 'format' => 's', 'return' => 'Vector' ],
'getkeys' => [ 'alias' => 'keys' ],
'migrate' => [ 'format' => 'slkll', 'return' => 'Boolean' ],
'move' => [ 'format' => 'kl', 'return' => '1' ],
'persist' => [ 'format' => 'k', 'return' => '1' ],
'randomkey' => [ 'return' => 'String' ],
'rename' => [ 'format' => 'kk', 'return' => 'Boolean' ],
'renamekey' => [ 'alias' => 'rename' ],
'renamenx' => [ 'format' => 'kk', 'return' => '1' ],
'type' => [ 'format' => 'k', 'return' => 'Type' ],
'ttl' => [ 'format' => 'k', 'return' => 'Long' ],
'pttl' => [ 'format' => 'k', 'return' => 'Long' ],
'restore' => [ 'format' => 'kls', 'return' => 'Boolean' ],
// Hashes
'hdel' => [ 'vararg' => self::VAR_KEY_FIRST, 'return' => 'Long' ],
'hexists' => [ 'format' => 'ks', 'return' => '1' ],
'hget' => [ 'format' => 'ks', 'return' => 'Serialized' ],
'hgetall' => [ 'format' => 'k', 'return' => 'Map',
'retargs' => [false,true] ],
'hincrby' => [ 'format' => 'ksl', 'return' => 'Long' ],
'hincrbyfloat' => [ 'format' => 'ksd', 'return' => 'Double' ],
'hkeys' => [ 'format' => 'k', 'return' => 'Vector' ],
'hlen' => [ 'format' => 'k', 'return' => 'Long' ],
'hset' => [ 'format' => 'ksv', 'return' => 'Long' ],
'hsetnx' => [ 'format' => 'ksv', 'return' => '1' ],
'hvals' => [ 'format' => 'k', 'return' => 'Vector', 'retargs' => [1] ],
// Lists
'blpop' => [ 'vararg' => self::VAR_KEY_ALL_AND_TIMEOUT,
'return' => 'Vector', 'retargs' => [1] ],
'brpop' => [ 'vararg' => self::VAR_KEY_ALL_AND_TIMEOUT,
'return' => 'Vector', 'retargs' => [1] ],
'brpoplpush' => [ 'format' => 'kkl', 'return' => 'Serialized' ],
'lindex' => [ 'format' => 'kl', 'return' => 'Serialized' ],
'lget' => [ 'alias' => 'lindex' ],
'linsert' => [ 'format' => 'kpkv', 'return' => 'Long' ],
'llen' => [ 'format' => 'k', 'return' => 'Long', 'cmd' => 'LLEN' ],
'lsize' => [ 'alias' => 'llen' ],
'lpop' => [ 'format' => 'k', 'return' => 'Serialized' ],
'lpush' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE,
'return' => 'Long' ],
'lpushx' => [ 'format' => 'kl', 'return' => 'Long' ],
'lrange' => [ 'format' => 'kll', 'return' => 'Vector', 'retargs' => [1] ],
'lgetrange' => [ 'alias' => 'lrange' ],
'lrem' => [ 'format' => 'kvl', 'return' => 'Long' ],
'lremove' => [ 'alias' => 'lrem' ],
'lset' => [ 'format' => 'klv', 'return' => 'Boolean' ],
'ltrim' => [ 'format' => 'kll', 'return' => 'Boolean' ],
'listtrim' => [ 'alias' => 'ltrim' ],
'rpop' => [ 'format' => 'k', 'return' => 'Serialized' ],
'rpoplpush' => [ 'format' => 'kk', 'return' => 'Serialized' ],
'rpush' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE,
'return' => 'Long' ],
'rpushx' => [ 'format' => 'kl', 'return' => 'Long' ],
// Sets
'sadd' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE,
'return' => 'Long' ],
'scard' => [ 'format' => 'k', 'return' => 'Long' ],
'ssize' => [ 'alias' => 'scard' ],
'sdiff' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Vector' ],
'sdiffstore' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Long' ],
'sinter' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Vector' ],
'sinterstore' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Long' ],
'sismember' => [ 'format' => 'kv', 'return' => '1' ],
'scontains' => [ 'alias' => 'sismember' ],
'smembers' => [ 'format' => 'k', 'return' => 'Vector' ],
'sgetmembers' => [ 'alias' => 'smembers' ],
'smove' => [ 'format' => 'kkv', 'return' => '1' ],
'spop' => [ 'format' => 'k', 'return' => 'Serialized' ],
'srandmember' => [ 'format' => 'kl', 'return' => 'Serialized',
'default' => [ 1 => 1 ] ],
'srem' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE,
'return' => 'Long' ],
'sremove' => [ 'alias' => 'srem' ],
'sunion' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Vector' ],
'sunionstore' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Long' ],
// zSets
'zcard' => [ 'format' => 'k', 'return' => 'Long' ],
'zsize' => [ 'alias' => 'zcard' ],
'zcount' => [ 'format' => 'kss', 'return' => 'Long' ],
'zincrby' => [ 'format' => 'kdv', 'return' => 'Double' ],
'zinter' => [ 'alias' => 'zinterstore' ],
'zunion' => [ 'alias' => 'zunionstore' ],
'zrank' => [ 'format' => 'kv', 'return' => 'Long' ],
'zrevrank' => [ 'format' => 'kv', 'return' => 'Long' ],
'zrem' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE,
'return' => 'Long' ],
'zremove' => [ 'alias' => 'zrem' ],
'zdelete' => [ 'alias' => 'zrem' ],
'zremrangebyrank' => [ 'format' => 'kll', 'return' => 'Long' ],
'zdeleterangebyrank' => [ 'alias' => 'zremrangebyrank' ],
'zremrangebyscore' => [ 'format' => 'kll', 'return' => 'Long' ],
'zdeleterangebyscore' => [ 'alias' => 'zremrangebyscore' ],
'zreverserange' => [ 'alias' => 'zrevrange' ],
'zscore' => [ 'format' => 'kv', 'return' => 'Double' ],
// Publish
'publish' => [ 'format' => 'kv', 'return' => 'Long' ],
/* These APIs are listed as "subject to change", avoid for now */
'subscribe' => false,
'psubscribe' => false,
'unsubscribe' => false,
'punsubscribe' => false,
// Introspection
'_prefix' => [ 'alias' => 'prefix' ],
'_unserialize' => [ 'alias' => 'unserialize' ],
// Batch Ops
'mget' => [ 'vararg' => self::VAR_KEY_ALL,
'return' => 'Vector', 'retargs' => [1] ],
'getmultiple' => [ 'alias' => 'mget' ],
];
/* Internal Use Only beyond this point --------------------------------- */
protected $host = '';
protected $port = -1;
protected $password = '';
protected $dbNumber = 0;
protected $last_connect = -1;
protected $retry_interval = 0;
protected $persistent = false;
protected $connection = null;
protected $lastError = '';
protected $timeout_connect = 0;
protected $timeout_seconds = 0;
protected $timeout_useconds = 0;
protected $mode = self::ATOMIC;
protected $multiHandler = [];
protected $commands = [];
protected $prefix = '';
protected $serializer = self::SERIALIZER_NONE;
/* protocol ------------------------------------------------------------ */
/* Internal use constants for var arg parsing */
const VAR_KEY_NONE = 0;
const VAR_KEY_FIRST = 1;
const VAR_KEY_NOT_FIRST = 2;
const VAR_KEY_ALL = 3;
const VAR_KEY_MASK = 0x000F;
const VAR_SERIALIZE = 0x0010;
const VAR_TIMEOUT = 0x0020;
const VAR_KEY_FIRST_AND_SERIALIZE = 0x0011;
const VAR_KEY_ALL_AND_TIMEOUT = 0x0023;
/* Returned by reference from Redis::sockReadData()
* Depending on the type of data returned by the server
*/
const TYPE_LINE = '+';
const TYPE_INT = ':';
const TYPE_ERR = '-';
const TYPE_BULK = '$';
const TYPE_MULTIBULK = '*';
protected function checkConnection($auto_reconnect = true) {
if ($this->connection AND !feof($this->connection)) {
// Connection seems fine
return true;
}
if ((time() - $this->last_connect) < $this->retry_interval) {
// We've tried connecting too recently, don't retry
return false;
}
if ($auto_reconnect AND
$this->doConnect($this->host, $this->port,
$this->timeout_connect,
null, $this->retry_internal,
$this->persistent)) {
if ($this->password) {
$this->auth($this->password);
}
if ($this->dbNumber) {
$this->select($this->dbNumber);
}
return true;
}
// Reconnect failed, give up
return false;
}
protected function sockReadLine() {
if (!$this->checkConnection()) {
return false;
}
$line = fgets($this->connection);
if (substr($line, -2) == "\r\n") {
$line = substr($line, 0, -2);
}
return $line;
}
protected function sockReadData(&$type) {
$line = $this->sockReadLine();
if (strlen($line)) {
$type = $line[0];
$line = substr($line, 1);
switch ($type) {
case self::TYPE_ERR:
if (!strncmp($line, '-ERR SYNC ', 10)) {
throw new RedisException("Sync with master in progress");
}
return $line;
case self::TYPE_INT:
case self::TYPE_LINE:
case self::TYPE_MULTIBULK: // Count of elements to follow
return $line;
case self::TYPE_BULK:
$bytes = (int)$line;
if ($bytes < 0) return null;
$buf = '';
while (strlen($buf) < ($bytes + 2)) {
$buf .= fread($this->connection, ($bytes + 2) - strlen($buf));
if (!$this->checkConnection()) {
return null;
}
}
return substr($buf, 0, -2);
default:
throw new RedisException("protocol error, got '{$type}' ".
"as reply type byte");
}
}
return null;
}
/**
* Process arguments for variadic functions based on $flags
*
* Redis::VAR_TIMEOUT indicates that the last argument
* in the list should be treated as an integer timeout
* for the operation
* Redis::VAR_KEY_* indicates which (NONE, FIRST, NOT_FIRST, ALL)
* of the arguments (excluding TIMEOUT, as application)
* should be treated as keys, and thus prefixed with Redis::$prefix
* Redis::VAR_SERIALIZE indicates that all non-timeout/non-key
* fields are data values, and should be serialzed
* (if a serialzied is specified)
*/
protected function translateVarArgs(array $args, $flags) {
// Check alternate vararg schemes first
if (($flags & self::VAR_TIMEOUT) AND
(count($args) == 2) AND
(is_array($args[0])) AND
(is_int($args[1]))) {
$args = $args[0] + [$args[1]];
}
if ((!($flags & self::VAR_TIMEOUT)) AND
(count($args) == 1) AND
(is_array($args[0]))) {
$args = $args[0];
}
// Then prefix, serialie, and cast as needed
if ($flags & self::VAR_TIMEOUT) {
$timeout = array_pop($args);
}
if (($this->prefix AND ($flags & self::VAR_KEY_MASK)) OR
($flags & self::VAR_SERIALIZE)) {
$first = true;
$varkey = $flags & self::VAR_KEY_MASK;
foreach($args as &$arg) {
if (( $first AND ($varkey == self::VAR_KEY_FIRST)) OR
(!$first AND ($varkey == self::VAR_KEY_NOT_FIRST)) OR
($varkey == self::VAR_KEY_ALL)) {
$arg = $this->prefix($arg);
} else if ($flags & self::VAR_SERIALIZE) {
$arg = $this->serialize($arg);
}
$first = false;
}
}
if ($flags & self::VAR_TIMEOUT) {
$args[] = (int)$timeout;
}
return $args;
}
/**
* Actually send a command to the server.
* assumes all appropriate prefixing and serialization
* has been preformed by the caller and constructs
* a Redis Protocol packet in the form:
*
* *N\r\n
*
* Folled by N instances of:
*
* $L\r\nA
*
* Where L is the length in bytes of argument A.
*
* So for the command `GET somekey` we'd serialize as:
*
* "*2\r\n$3\r\nGET\r\n$7\r\nsomekey\r\n"
*/
protected function processArrayCommand($cmd, array $args) {
if ($this->mode == self::PIPELINE) {
$this->commands[] = [ 'cmd' => $cmd, 'args' => $args ];
return true;
}
$clen = strlen($cmd);
$count = count($args) + 1;
$cmd = "*{$count}\r\n\${$clen}\r\n{$cmd}\r\n";
while (count($args)) {
$arg = (string)array_shift($args);
$alen = strlen($arg);
$cmd .= "\${$alen}\r\n{$arg}\r\n";
}
if (!$this->checkConnection()) {
return false;
}
return (bool)fwrite($this->connection, $cmd);
}
protected function processCommand($cmd, /* ... */) {
$args = func_get_args();
array_shift($args);
return $this->processArrayCommand($cmd, $args);
}
protected function serialize($str) {
switch ($this->serializer) {
case self::SERIALIZER_NONE:
return $str;
case self::SERIALIZER_PHP:
return serialize($str);
case self::SERIALIZER_IGBINARY:
default:
throw new RedisException("Not Implemented");
}
}
protected function unserialize($str) {
switch ($this->serializer) {
case self::SERIALIZER_NONE:
return $str;
case self::SERIALIZER_PHP:
return unserialize($str);
case self::SERIALIZER_IGBINARY:
default:
throw new RedisException("Not Implemented");
}
}
protected function processVariantResponse() {
if ($this->mode === self::ATOMIC) {
return $this->sockReadData($type);
}
$this->multiHandler[] = [ 'cb' => [$this,'processVariantResponse'] ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
protected function processClientListResponse() {
if ($this->mode !== self::ATOMIC) {
$this->multiHandler[] = [ 'cb' => [$this,'processClientListResponse'] ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
$resp = $this->sockReadData($type);
if (($type !== self::TYPE_LINE) AND
($type !== self::TYPE_BULK)) {
return null;
}
$ret = [];
$pairs = explode(' ', trim($resp));
foreach ($pairs as $pair) {
$kv = explode('=', $pair, 2);
if (count($kv) == 1) {
$ret[] = $pair;
} else {
list($k, $v) = $kv;
$ret[$k] = $v;
}
}
return $ret;
}
protected function processSerializedResponse() {
if ($this->mode === self::ATOMIC) {
$resp = $this->sockReadData($type);
return (($type === self::TYPE_LINE) OR ($type === self::TYPE_BULK))
? $this->unserialize($resp) : null;
}
$this->multiHandler[] = [ 'cb' => [$this,'processSerializedResponse'] ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
protected function processBooleanResponse() {
if ($this->mode === self::ATOMIC) {
$resp = $this->sockReadData($type);
return ($type === self::TYPE_LINE) AND ($resp === 'OK');
}
$this->multiHandler[] = [ 'cb' => [$this,'processBooleanResponse'] ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
protected function processLongResponse() {
if ($this->mode === self::ATOMIC) {
$resp = $this->sockReadData($type);
return ($type === self::TYPE_INT) ? ((int)$resp) : null;
}
$this->multiHandler[] = [ 'cb' => [$this,'processLongResponse'] ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
protected function processDoubleResponse() {
if ($this->mode === self::ATOMIC) {
$resp = $this->sockReadData($type);
return ($type === self::TYPE_INT) ? ((float)$resp) : null;
}
$this->multiHandler[] = [ 'cb' => [$this,'processDoubleResponse'] ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
protected function processStringResponse() {
if ($this->mode === self::ATOMIC) {
$resp = $this->sockReadData($type);
return (($type === self::TYPE_LINE) OR ($type === self::TYPE_BULK))
? ((string)$resp) : null;
}
$this->multiHandler[] = [ 'cb' => [$this,'processStringResponse'] ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
protected function processVectorResponse($unser = 0) {
if ($this->mode !== self::ATOMIC) {
$this->multiHandler[] = [ 'cb' => [$this, 'processVectorResponse'],
'args' => [$unser]
];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
$count = $this->sockReadData($type);
if ($type !== self::TYPE_MULTIBULK) {
return null;
}
$ret = [];
$lineNo = 0;
while($count--) {
$lineNo++;
$val = $this->sockReadData($type);
if ($unser AND (($lineNo % $unser) == 0)) {
$val = $this->unserialize($val);
}
$ret[] = $val;
}
return $ret;
}
protected function processMapResponse($unser_key, $unser_val = true) {
if ($this->mode !== self::ATOMIC) {
$this->multiHandler[] = [ 'cb' => [$this, 'processMapResponse'],
'args' => [$unser_key,$unser_val]
];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
$count = $this->sockReadData($type);
if ($type !== self::TYPE_MULTIBULK) {
return null;
}
$ret = [];
while($count > 1) {
$key = $this->sockReadData($type);
if ($unser_key) {
$key = $this->unserialize($key);
}
$val = $this->sockReadData($type);
if ($unser_val) {
$val = $this->unserialize($val);
}
$ret[$key] = $val;
$count -= 2;
}
if ($count > 1) {
$ret[$this->sockReadData($type)] = null;
}
return $ret;
}
protected function processAssocResponse(array $keys, $unser_val = true) {
if ($this->mode !== self::ATOMIC) {
$this->multiHandler[] = [ 'cb' => [$this, 'processAssocResponse'],
'args' => [$keys, $unser_val]
];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
$count = $this->sockReadData($type);
if ($type !== self::TYPE_MULTIBULK) {
return null;
}
$ret = [];
while($count--) {
$key = array_shift($keys);
$val = $this->sockReadLine();
if ($unser_val) {
$val = $this->unserialize($val);
}
$ret[$key] = $val;
}
return $ret;
}
protected function process1Response() {
if ($this->mode === self::ATOMIC) {
$resp = $this->sockReadData($type);
return ($type === self::TYPE_INT) && ($resp === '1');
}
$this->multiHandler[] = [ 'cb' => [$this,'process1Response'] ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
protected function processTypeResponse() {
if ($this->mode === self::ATOMIC) {
$resp = $this->sockReadData($type);
if ($type !== self::TYPE_LINE) {
return self::REDIS_NOT_FOUND;
}
switch($resp) {
case 'string': return self::REDIS_STRING;
case 'set': return self::REDIS_SET;
case 'list': return self::REDIS_LIST;
case 'zset': return self::REDIS_ZSET;
case 'hash': return self::REDIS_HASH;
default: return self::REDIS_NOT_FOUND;
}
}
$this->multiHandler[] = [ 'cb' => 'processTypeResponse' ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
protected function processRawResponse() {
if ($this->mode === self::ATOMIC) {
return $this->sockReadLine();
}
$this->multiHandler[] = [ 'cb' => 'processRawResponse' ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
protected function processInfoResponse() {
if ($this->mode !== self::ATOMIC) {
$this->multiHandler[] = [ 'cb' => 'processInfoResponse' ];
if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) {
return false;
}
return $this;
}
$resp = $this->sockReadData($type);
if (($type !== self::TYPE_LINE) AND ($type !== self::TYPE_BULK)) {
return false;
}
$ret = [];
$lines = preg_split('/[\r\n]+/', $resp);
foreach ($lines as $line) {
if ((substr($line, 0, 1) == '#') OR
!trim($line)) {
continue;
}
$colon = strpos($line, ':');
if ($colon === false) {
break;
}
list($key, $val) = explode(':', $line, 2);
$ret[$key] = $val;
}
return $ret;
}
protected function processQueuedResponse() {
$resp = $this->sockReadData($type);
return ($type === self::TYPE_LINE) AND ($resp === 'QUEUED');
}
protected function prefix($key) {
return $this->prefix . $key;
}
/**
* Dispatches all commands in the Redis::$map list
*
* All other commands are handled by explicit implementations
*/
public function __call($fname, $args) {
$fname = strtolower($fname);
if (!isset(self::$map[$fname])) {
error_log("Call to undefined function Redis::$fname", E_USER_ERROR);
return null;
}
$func = self::$map[$fname];
if ($func === false) {
throw new RedisException("Redis::$fname() is currently unimplemented");
}
// Normalize record
if (!empty($func['alias'])) {
if (isset(self::$map[$func['alias']])) {
$fname = $func['alias'];
$func = self::$map[$fname];
} else {
return call_user_func_array([$this,$func['alias']],func_get_args());
}
}
if (empty($func['format'])) {
$func['format'] = isset($func['vararg']) ? '...' : '';
}
if (empty($func['cmd'])) {
$func['cmd'] = strtoupper($fname);
}
if (empty($func['handler'])) {
$func['handler'] = empty($func['return'])
? null : "process{$func['return']}Response";
}
if (empty($func['retargs'])) {
$func['retargs'] = [];
}
$format = $func['format'];
$argc = count($args);
if ($format == '...') {
$args = $this->translateVarArgs($args, $func['vararg']);
$this->processArrayCommand($func['cmd'], $args);
if (empty($func['handler'])) {
return null;
}
return call_user_func_array([$this, $func['handler']], $func['retargs']);
}
$flen = strlen($format);
for ($i = 0; $i < $flen; $i++) {
if (!isset($args[$i])) {
if (isset($func['defaults']) AND
array_key_exists($func['defaults'], $i)) {
$args[$i] = $func['defualts'][$i];
} else {
error_log("Redis::$fname requires at least $flen parameters ".
"$argc given", E_USER_ERROR);
return null;
}
}
switch ($format[$i]) {
case 'k': $args[$i] = $this->prefix($args[$i]); break;
case 'v': $args[$i] = $this->serialize($args[$i]); break;
case 's': $args[$i] = (string)$args[$i]; break;
case 'l': $args[$i] = (int)$args[$i]; break;
case 'd': $args[$i] = (float)$args[$i]; break;
case 'b': $args[$i] = (bool)$arts[$i]; break;
case 'p':
if (($args[$i] !== self::BEFORE) AND ($args[$i] !== self::AFTER)) {
error_log("Argument $i to Redis::$fname must be ".
"'before' or 'after'", E_USER_ERROR);
return null;
} break;
}
}
$this->processArrayCommand($func['cmd'], $args);
if (empty($func['handler'])) {
return null;
}
return call_user_func_array([$this, $func['handler']], $func['retargs']);
}
/* --------------------------------------------------------------------- */
protected function doConnect($host,
$port,
$timeout,
$persistent_id,
$retry_interval,
$persistent = false) {
if (!empty($persistent_id)) {
throw new RedisException("Named persistent connections not supported");
}
if (($port <= 0) && (substr($host, 0, 1) != '/')) {
$port = self::DEFAULT_PORT;
}
if ($persistent) {
$conn = pfsockopen($host, $port, $errno, $errstr, $timeout);
} else {
$conn = fsockopen($host, $port, $errno, $errstr, $timeout);
}
$this->last_connect = time();
$this->host = $host;
$this->port = $port;
$this->retry_interval = $retry_interval;
$this->timeout_connect = $timeout;
$this->persistent = $persistent;
$this->connection = $conn;
$this->dbNumber = 0;
$this->commands = [];
$this->multiHandler = [];
$this->mode = self::ATOMIC;
if (!$conn) {
error_log("Failed connecting to redis server at {$host}: {$errstr}",
E_USER_WARNING);
return false;
}
stream_set_blocking($conn, true);
stream_set_timeout($conn, $this->timeout_seconds, $this->timeout_useconds);
return true;
}
protected function sortClause(array $arr, &$using_store) {
$using_store = false;
if (!$arr) {
return [];
}
$ret = [];
foreach(['by','sort','store','get','alpha','limit','dir'] as $k) {
if (isset($arr[$k])) {
$v = $arr[$k];
} else if (isset($arr[strtoupper($k)])) {
$v = $arr[strtoupper($k)];
} else {
continue;
}
if (($k == 'get') AND is_array($v)) {
foreach ($v as $val) {
$ret[] = 'GET';
$ret[] = $val;
}
continue;
}
if ($k == 'alpha') {
if ($v === true) {
$ret[] = 'ALPHA';
}
continue;
}
if ($k == 'limit') {
if (is_array($val) AND (count($val) == 2)) {
list($off, $cnt) = $val;
$ret[] = 'LIMIT';
$ret[] = $off;
$ret[] = $cnt;
}
continue;
}
if ($k == 'store') {
$using_store = true;
}
if ($k == 'dir') {
$ret[] = strtoupper($v);
continue;
}
$ret[] = strtoupper($k);
$ret[] = $v;
}
return $ret;
}
public function __destruct() {
}
}