Файловый менеджер - Редактировать - /home/rowdyr7/vbln.supply/wp-content/plugins/nginx-helper/admin/predis.php
Назад
<?php // phpcs:disable /* * This file is part of the Predis package. * * (c) Daniele Alessandri <suppakilla@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Predis\Command; use InvalidArgumentException; /** * Defines an abstraction representing a Redis command. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface CommandInterface { /** * Returns the ID of the Redis command. By convention, command identifiers * must always be uppercase. * * @return string */ public function getId(); /** * Assign the specified slot to the command for clustering distribution. * * @param int $slot Slot ID. */ public function setSlot($slot); /** * Returns the assigned slot of the command for clustering distribution. * * @return int|null */ public function getSlot(); /** * Sets the arguments for the command. * * @param array $arguments List of arguments. */ public function setArguments(array $arguments); /** * Sets the raw arguments for the command without processing them. * * @param array $arguments List of arguments. */ public function setRawArguments(array $arguments); /** * Gets the arguments of the command. * * @return array */ public function getArguments(); /** * Gets the argument of the command at the specified index. * * @param int $index Index of the desired argument. * * @return mixed|null */ public function getArgument($index); /** * Parses a raw response and returns a PHP object. * * @param string $data Binary string containing the whole response. * * @return mixed */ public function parseResponse($data); } /** * Base class for Redis commands. * * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class Command implements CommandInterface { private $slot; private $arguments = array(); /** * Returns a filtered array of the arguments. * * @param array $arguments List of arguments. * * @return array */ protected function filterArguments(array $arguments) { return $arguments; } /** * {@inheritdoc} */ public function setArguments(array $arguments) { $this->arguments = $this->filterArguments($arguments); unset($this->slot); } /** * {@inheritdoc} */ public function setRawArguments(array $arguments) { $this->arguments = $arguments; unset($this->slot); } /** * {@inheritdoc} */ public function getArguments() { return $this->arguments; } /** * {@inheritdoc} */ public function getArgument($index) { if (isset($this->arguments[$index])) { return $this->arguments[$index]; } } /** * {@inheritdoc} */ public function setSlot($slot) { $this->slot = $slot; } /** * {@inheritdoc} */ public function getSlot() { if (isset($this->slot)) { return $this->slot; } } /** * {@inheritdoc} */ public function parseResponse($data) { return $data; } /** * Normalizes the arguments array passed to a Redis command. * * @param array $arguments Arguments for a command. * * @return array */ public static function normalizeArguments(array $arguments) { if (count($arguments) === 1 && is_array($arguments[0])) { return $arguments[0]; } return $arguments; } /** * Normalizes the arguments array passed to a variadic Redis command. * * @param array $arguments Arguments for a command. * * @return array */ public static function normalizeVariadic(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { return array_merge(array($arguments[0]), $arguments[1]); } return $arguments; } } /** * @link http://redis.io/commands/zrange * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetRange extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZRANGE'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 4) { $lastType = gettype($arguments[3]); if ($lastType === 'string' && strtoupper($arguments[3]) === 'WITHSCORES') { // Used for compatibility with older versions $arguments[3] = array('WITHSCORES' => true); $lastType = 'array'; } if ($lastType === 'array') { $options = $this->prepareOptions(array_pop($arguments)); return array_merge($arguments, $options); } } return $arguments; } /** * Returns a list of options and modifiers compatible with Redis. * * @param array $options List of options. * * @return array */ protected function prepareOptions($options) { $opts = array_change_key_case($options, CASE_UPPER); $finalizedOpts = array(); if (!empty($opts['WITHSCORES'])) { $finalizedOpts[] = 'WITHSCORES'; } return $finalizedOpts; } /** * Checks for the presence of the WITHSCORES modifier. * * @return bool */ protected function withScores() { $arguments = $this->getArguments(); if (count($arguments) < 4) { return false; } return strtoupper($arguments[3]) === 'WITHSCORES'; } /** * {@inheritdoc} */ public function parseResponse($data) { if ($this->withScores()) { $result = array(); for ($i = 0; $i < count($data); $i++) { $result[$data[$i]] = $data[++$i]; } return $result; } return $data; } } /** * @link http://redis.io/commands/sinterstore * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetIntersectionStore extends Command { /** * {@inheritdoc} */ public function getId() { return 'SINTERSTORE'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { return array_merge(array($arguments[0]), $arguments[1]); } return $arguments; } } /** * @link http://redis.io/commands/sinter * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetIntersection extends Command { /** * {@inheritdoc} */ public function getId() { return 'SINTER'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeArguments($arguments); } } /** * @link http://redis.io/commands/eval * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerEval extends Command { /** * {@inheritdoc} */ public function getId() { return 'EVAL'; } /** * Calculates the SHA1 hash of the body of the script. * * @return string SHA1 hash. */ public function getScriptHash() { return sha1($this->getArgument(0)); } } /** * @link http://redis.io/commands/rename * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyRename extends Command { /** * {@inheritdoc} */ public function getId() { return 'RENAME'; } } /** * @link http://redis.io/commands/setex * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringSetExpire extends Command { /** * {@inheritdoc} */ public function getId() { return 'SETEX'; } } /** * @link http://redis.io/commands/mset * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringSetMultiple extends Command { /** * {@inheritdoc} */ public function getId() { return 'MSET'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 1 && is_array($arguments[0])) { $flattenedKVs = array(); $args = $arguments[0]; foreach ($args as $k => $v) { $flattenedKVs[] = $k; $flattenedKVs[] = $v; } return $flattenedKVs; } return $arguments; } } /** * @link http://redis.io/commands/expireat * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyExpireAt extends Command { /** * {@inheritdoc} */ public function getId() { return 'EXPIREAT'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/blpop * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPopFirstBlocking extends Command { /** * {@inheritdoc} */ public function getId() { return 'BLPOP'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 2 && is_array($arguments[0])) { list($arguments, $timeout) = $arguments; array_push($arguments, $timeout); } return $arguments; } } /** * @link http://redis.io/commands/unsubscribe * @author Daniele Alessandri <suppakilla@gmail.com> */ class PubSubUnsubscribe extends Command { /** * {@inheritdoc} */ public function getId() { return 'UNSUBSCRIBE'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeArguments($arguments); } } /** * @link http://redis.io/commands/info * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerInfo extends Command { /** * {@inheritdoc} */ public function getId() { return 'INFO'; } /** * {@inheritdoc} */ public function parseResponse($data) { $info = array(); $infoLines = preg_split('/\r?\n/', $data); foreach ($infoLines as $row) { if (strpos($row, ':') === false) { continue; } list($k, $v) = $this->parseRow($row); $info[$k] = $v; } return $info; } /** * Parses a single row of the response and returns the key-value pair. * * @param string $row Single row of the response. * * @return array */ protected function parseRow($row) { list($k, $v) = explode(':', $row, 2); if (preg_match('/^db\d+$/', $k)) { $v = $this->parseDatabaseStats($v); } return array($k, $v); } /** * Extracts the statistics of each logical DB from the string buffer. * * @param string $str Response buffer. * * @return array */ protected function parseDatabaseStats($str) { $db = array(); foreach (explode(',', $str) as $dbvar) { list($dbvk, $dbvv) = explode('=', $dbvar); $db[trim($dbvk)] = $dbvv; } return $db; } /** * Parses the response and extracts the allocation statistics. * * @param string $str Response buffer. * * @return array */ protected function parseAllocationStats($str) { $stats = array(); foreach (explode(',', $str) as $kv) { @list($size, $objects, $extra) = explode('=', $kv); // hack to prevent incorrect values when parsing the >=256 key if (isset($extra)) { $size = ">=$objects"; $objects = $extra; } $stats[$size] = $objects; } return $stats; } } /** * @link http://redis.io/commands/evalsha * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerEvalSHA extends ServerEval { /** * {@inheritdoc} */ public function getId() { return 'EVALSHA'; } /** * Returns the SHA1 hash of the body of the script. * * @return string SHA1 hash. */ public function getScriptHash() { return $this->getArgument(0); } } /** * @link http://redis.io/commands/expire * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyExpire extends Command { /** * {@inheritdoc} */ public function getId() { return 'EXPIRE'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/subscribe * @author Daniele Alessandri <suppakilla@gmail.com> */ class PubSubSubscribe extends Command { /** * {@inheritdoc} */ public function getId() { return 'SUBSCRIBE'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeArguments($arguments); } } /** * @link http://redis.io/commands/rpush * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPushTail extends Command { /** * {@inheritdoc} */ public function getId() { return 'RPUSH'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeVariadic($arguments); } } /** * @link http://redis.io/commands/ttl * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyTimeToLive extends Command { /** * {@inheritdoc} */ public function getId() { return 'TTL'; } } /** * @link http://redis.io/commands/zunionstore * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetUnionStore extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZUNIONSTORE'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { $options = array(); $argc = count($arguments); if ($argc > 2 && is_array($arguments[$argc - 1])) { $options = $this->prepareOptions(array_pop($arguments)); } if (is_array($arguments[1])) { $arguments = array_merge( array($arguments[0], count($arguments[1])), $arguments[1] ); } return array_merge($arguments, $options); } /** * Returns a list of options and modifiers compatible with Redis. * * @param array $options List of options. * * @return array */ private function prepareOptions($options) { $opts = array_change_key_case($options, CASE_UPPER); $finalizedOpts = array(); if (isset($opts['WEIGHTS']) && is_array($opts['WEIGHTS'])) { $finalizedOpts[] = 'WEIGHTS'; foreach ($opts['WEIGHTS'] as $weight) { $finalizedOpts[] = $weight; } } if (isset($opts['AGGREGATE'])) { $finalizedOpts[] = 'AGGREGATE'; $finalizedOpts[] = $opts['AGGREGATE']; } return $finalizedOpts; } } /** * @link http://redis.io/commands/zrangebyscore * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetRangeByScore extends ZSetRange { /** * {@inheritdoc} */ public function getId() { return 'ZRANGEBYSCORE'; } /** * {@inheritdoc} */ protected function prepareOptions($options) { $opts = array_change_key_case($options, CASE_UPPER); $finalizedOpts = array(); if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) { $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER); $finalizedOpts[] = 'LIMIT'; $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0]; $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1]; } return array_merge($finalizedOpts, parent::prepareOptions($options)); } /** * {@inheritdoc} */ protected function withScores() { $arguments = $this->getArguments(); for ($i = 3; $i < count($arguments); $i++) { switch (strtoupper($arguments[$i])) { case 'WITHSCORES': return true; case 'LIMIT': $i += 2; break; } } return false; } } /** * @link http://redis.io/commands/zremrangebyrank * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetRemoveRangeByRank extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZREMRANGEBYRANK'; } } /** * @link http://redis.io/commands/spop * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetPop extends Command { /** * {@inheritdoc} */ public function getId() { return 'SPOP'; } } /** * @link http://redis.io/commands/smove * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetMove extends Command { /** * {@inheritdoc} */ public function getId() { return 'SMOVE'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/sismember * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetIsMember extends Command { /** * {@inheritdoc} */ public function getId() { return 'SISMEMBER'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/smembers * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetMembers extends Command { /** * {@inheritdoc} */ public function getId() { return 'SMEMBERS'; } } /** * @link http://redis.io/commands/zremrangebyscore * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetRemoveRangeByScore extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZREMRANGEBYSCORE'; } } /** * @link http://redis.io/commands/srandmember * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetRandomMember extends Command { /** * {@inheritdoc} */ public function getId() { return 'SRANDMEMBER'; } } /** * @link http://redis.io/commands/sscan * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetScan extends Command { /** * {@inheritdoc} */ public function getId() { return 'SSCAN'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 3 && is_array($arguments[2])) { $options = $this->prepareOptions(array_pop($arguments)); $arguments = array_merge($arguments, $options); } return $arguments; } /** * Returns a list of options and modifiers compatible with Redis. * * @param array $options List of options. * * @return array */ protected function prepareOptions($options) { $options = array_change_key_case($options, CASE_UPPER); $normalized = array(); if (!empty($options['MATCH'])) { $normalized[] = 'MATCH'; $normalized[] = $options['MATCH']; } if (!empty($options['COUNT'])) { $normalized[] = 'COUNT'; $normalized[] = $options['COUNT']; } return $normalized; } } /** * @link http://redis.io/commands/zremrangebylex * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetRemoveRangeByLex extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZREMRANGEBYLEX'; } } /** * @link http://redis.io/commands/bitop * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringBitOp extends Command { /** * {@inheritdoc} */ public function getId() { return 'BITOP'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 3 && is_array($arguments[2])) { list($operation, $destination, ) = $arguments; $arguments = $arguments[2]; array_unshift($arguments, $operation, $destination); } return $arguments; } } /** * @link http://redis.io/commands/bitcount * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringBitCount extends Command { /** * {@inheritdoc} */ public function getId() { return 'BITCOUNT'; } } /** * @link http://redis.io/commands/append * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringAppend extends Command { /** * {@inheritdoc} */ public function getId() { return 'APPEND'; } } /** * @link http://redis.io/commands/sunion * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetUnion extends SetIntersection { /** * {@inheritdoc} */ public function getId() { return 'SUNION'; } } /** * @link http://redis.io/commands/sunionstore * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetUnionStore extends SetIntersectionStore { /** * {@inheritdoc} */ public function getId() { return 'SUNIONSTORE'; } } /** * @link http://redis.io/commands/srem * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetRemove extends Command { /** * {@inheritdoc} */ public function getId() { return 'SREM'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeVariadic($arguments); } } /** * @link http://redis.io/commands/zrevrange * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetReverseRange extends ZSetRange { /** * {@inheritdoc} */ public function getId() { return 'ZREVRANGE'; } } /** * @link http://redis.io/commands/slowlog * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerSlowlog extends Command { /** * {@inheritdoc} */ public function getId() { return 'SLOWLOG'; } /** * {@inheritdoc} */ public function parseResponse($data) { if (is_array($data)) { $log = array(); foreach ($data as $index => $entry) { $log[$index] = array( 'id' => $entry[0], 'timestamp' => $entry[1], 'duration' => $entry[2], 'command' => $entry[3], ); } return $log; } return $data; } } /** * @link http://redis.io/commands/zscore * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetScore extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZSCORE'; } } /** * @link http://redis.io/commands/slaveof * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerSlaveOf extends Command { /** * {@inheritdoc} */ public function getId() { return 'SLAVEOF'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 0 || $arguments[0] === 'NO ONE') { return array('NO', 'ONE'); } return $arguments; } } /** * @link http://redis.io/commands/shutdown * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerShutdown extends Command { /** * {@inheritdoc} */ public function getId() { return 'SHUTDOWN'; } } /** * @link http://redis.io/commands/script * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerScript extends Command { /** * {@inheritdoc} */ public function getId() { return 'SCRIPT'; } } /** * @link http://redis.io/topics/sentinel * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerSentinel extends Command { /** * {@inheritdoc} */ public function getId() { return 'SENTINEL'; } /** * {@inheritdoc} */ public function parseResponse($data) { switch (strtolower($this->getArgument(0))) { case 'masters': case 'slaves': return self::processMastersOrSlaves($data); default: return $data; } } /** * Returns a processed response to SENTINEL MASTERS or SENTINEL SLAVES. * * @param array $servers List of Redis servers. * * @return array */ protected static function processMastersOrSlaves(array $servers) { foreach ($servers as $idx => $node) { $processed = array(); $count = count($node); for ($i = 0; $i < $count; $i++) { $processed[$node[$i]] = $node[++$i]; } $servers[$idx] = $processed; } return $servers; } } /** * @link http://redis.io/commands/zscan * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetScan extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZSCAN'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 3 && is_array($arguments[2])) { $options = $this->prepareOptions(array_pop($arguments)); $arguments = array_merge($arguments, $options); } return $arguments; } /** * Returns a list of options and modifiers compatible with Redis. * * @param array $options List of options. * * @return array */ protected function prepareOptions($options) { $options = array_change_key_case($options, CASE_UPPER); $normalized = array(); if (!empty($options['MATCH'])) { $normalized[] = 'MATCH'; $normalized[] = $options['MATCH']; } if (!empty($options['COUNT'])) { $normalized[] = 'COUNT'; $normalized[] = $options['COUNT']; } return $normalized; } /** * {@inheritdoc} */ public function parseResponse($data) { if (is_array($data)) { $members = $data[1]; $result = array(); for ($i = 0; $i < count($members); $i++) { $result[$members[$i]] = (float) $members[++$i]; } $data[1] = $result; } return $data; } } /** * @link http://redis.io/commands/zrevrank * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetReverseRank extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZREVRANK'; } } /** * @link http://redis.io/commands/sdiffstore * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetDifferenceStore extends SetIntersectionStore { /** * {@inheritdoc} */ public function getId() { return 'SDIFFSTORE'; } } /** * @link http://redis.io/commands/zrevrangebyscore * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetReverseRangeByScore extends ZSetRangeByScore { /** * {@inheritdoc} */ public function getId() { return 'ZREVRANGEBYSCORE'; } } /** * @link http://redis.io/commands/sdiff * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetDifference extends SetIntersection { /** * {@inheritdoc} */ public function getId() { return 'SDIFF'; } } /** * @link http://redis.io/commands/scard * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetCardinality extends Command { /** * {@inheritdoc} */ public function getId() { return 'SCARD'; } } /** * @link http://redis.io/commands/time * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerTime extends Command { /** * {@inheritdoc} */ public function getId() { return 'TIME'; } } /** * @link http://redis.io/commands/sadd * @author Daniele Alessandri <suppakilla@gmail.com> */ class SetAdd extends Command { /** * {@inheritdoc} */ public function getId() { return 'SADD'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeVariadic($arguments); } } /** * @link http://redis.io/commands/bitpos * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringBitPos extends Command { /** * {@inheritdoc} */ public function getId() { return 'BITPOS'; } } /** * @link http://redis.io/commands/decrby * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringDecrementBy extends Command { /** * {@inheritdoc} */ public function getId() { return 'DECRBY'; } } /** * @link http://redis.io/commands/substr * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringSubstr extends Command { /** * {@inheritdoc} */ public function getId() { return 'SUBSTR'; } } /** * @link http://redis.io/commands/zlexcount * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetLexCount extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZLEXCOUNT'; } } /** * @link http://redis.io/commands/zinterstore * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetIntersectionStore extends ZSetUnionStore { /** * {@inheritdoc} */ public function getId() { return 'ZINTERSTORE'; } } /** * @link http://redis.io/commands/strlen * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringStrlen extends Command { /** * {@inheritdoc} */ public function getId() { return 'STRLEN'; } } /** * @link http://redis.io/commands/setrange * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringSetRange extends Command { /** * {@inheritdoc} */ public function getId() { return 'SETRANGE'; } } /** * @link http://redis.io/commands/msetnx * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringSetMultiplePreserve extends StringSetMultiple { /** * {@inheritdoc} */ public function getId() { return 'MSETNX'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/setnx * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringSetPreserve extends Command { /** * {@inheritdoc} */ public function getId() { return 'SETNX'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/discard * @author Daniele Alessandri <suppakilla@gmail.com> */ class TransactionDiscard extends Command { /** * {@inheritdoc} */ public function getId() { return 'DISCARD'; } } /** * @link http://redis.io/commands/exec * @author Daniele Alessandri <suppakilla@gmail.com> */ class TransactionExec extends Command { /** * {@inheritdoc} */ public function getId() { return 'EXEC'; } } /** * @link http://redis.io/commands/zcard * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetCardinality extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZCARD'; } } /** * @link http://redis.io/commands/zcount * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetCount extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZCOUNT'; } } /** * @link http://redis.io/commands/zadd * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetAdd extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZADD'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { $flattened = array($arguments[0]); foreach ($arguments[1] as $member => $score) { $flattened[] = $score; $flattened[] = $member; } return $flattened; } return $arguments; } } /** * @link http://redis.io/commands/watch * @author Daniele Alessandri <suppakilla@gmail.com> */ class TransactionWatch extends Command { /** * {@inheritdoc} */ public function getId() { return 'WATCH'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (isset($arguments[0]) && is_array($arguments[0])) { return $arguments[0]; } return $arguments; } } /** * @link http://redis.io/commands/multi * @author Daniele Alessandri <suppakilla@gmail.com> */ class TransactionMulti extends Command { /** * {@inheritdoc} */ public function getId() { return 'MULTI'; } } /** * @link http://redis.io/commands/unwatch * @author Daniele Alessandri <suppakilla@gmail.com> */ class TransactionUnwatch extends Command { /** * {@inheritdoc} */ public function getId() { return 'UNWATCH'; } } /** * @link http://redis.io/commands/zrangebylex * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetRangeByLex extends ZSetRange { /** * {@inheritdoc} */ public function getId() { return 'ZRANGEBYLEX'; } /** * {@inheritdoc} */ protected function prepareOptions($options) { $opts = array_change_key_case($options, CASE_UPPER); $finalizedOpts = array(); if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) { $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER); $finalizedOpts[] = 'LIMIT'; $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0]; $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1]; } return $finalizedOpts; } /** * {@inheritdoc} */ protected function withScores() { return false; } } /** * @link http://redis.io/commands/zrank * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetRank extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZRANK'; } } /** * @link http://redis.io/commands/mget * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringGetMultiple extends Command { /** * {@inheritdoc} */ public function getId() { return 'MGET'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeArguments($arguments); } } /** * @link http://redis.io/commands/getrange * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringGetRange extends Command { /** * {@inheritdoc} */ public function getId() { return 'GETRANGE'; } } /** * @link http://redis.io/commands/zrem * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetRemove extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZREM'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeVariadic($arguments); } } /** * @link http://redis.io/commands/getbit * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringGetBit extends Command { /** * {@inheritdoc} */ public function getId() { return 'GETBIT'; } } /** * @link http://redis.io/commands/zincrby * @author Daniele Alessandri <suppakilla@gmail.com> */ class ZSetIncrementBy extends Command { /** * {@inheritdoc} */ public function getId() { return 'ZINCRBY'; } } /** * @link http://redis.io/commands/get * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringGet extends Command { /** * {@inheritdoc} */ public function getId() { return 'GET'; } } /** * @link http://redis.io/commands/getset * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringGetSet extends Command { /** * {@inheritdoc} */ public function getId() { return 'GETSET'; } } /** * @link http://redis.io/commands/incr * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringIncrement extends Command { /** * {@inheritdoc} */ public function getId() { return 'INCR'; } } /** * @link http://redis.io/commands/set * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringSet extends Command { /** * {@inheritdoc} */ public function getId() { return 'SET'; } } /** * @link http://redis.io/commands/setbit * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringSetBit extends Command { /** * {@inheritdoc} */ public function getId() { return 'SETBIT'; } } /** * @link http://redis.io/commands/psetex * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringPreciseSetExpire extends StringSetExpire { /** * {@inheritdoc} */ public function getId() { return 'PSETEX'; } } /** * @link http://redis.io/commands/incrbyfloat * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringIncrementByFloat extends Command { /** * {@inheritdoc} */ public function getId() { return 'INCRBYFLOAT'; } } /** * @link http://redis.io/commands/incrby * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringIncrementBy extends Command { /** * {@inheritdoc} */ public function getId() { return 'INCRBY'; } } /** * @link http://redis.io/commands/save * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerSave extends Command { /** * {@inheritdoc} */ public function getId() { return 'SAVE'; } } /** * @link http://redis.io/commands/decr * @author Daniele Alessandri <suppakilla@gmail.com> */ class StringDecrement extends Command { /** * {@inheritdoc} */ public function getId() { return 'DECR'; } } /** * @link http://redis.io/commands/flushall * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerFlushAll extends Command { /** * {@inheritdoc} */ public function getId() { return 'FLUSHALL'; } } /** * @link http://redis.io/commands/del * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyDelete extends Command { /** * {@inheritdoc} */ public function getId() { return 'DEL'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeArguments($arguments); } } /** * @link http://redis.io/commands/dump * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyDump extends Command { /** * {@inheritdoc} */ public function getId() { return 'DUMP'; } } /** * @link http://redis.io/commands/exists * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyExists extends Command { /** * {@inheritdoc} */ public function getId() { return 'EXISTS'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/pfmerge * @author Daniele Alessandri <suppakilla@gmail.com> */ class HyperLogLogMerge extends Command { /** * {@inheritdoc} */ public function getId() { return 'PFMERGE'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeArguments($arguments); } } /** * @link http://redis.io/commands/pfcount * @author Daniele Alessandri <suppakilla@gmail.com> */ class HyperLogLogCount extends Command { /** * {@inheritdoc} */ public function getId() { return 'PFCOUNT'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeArguments($arguments); } } /** * @link http://redis.io/commands/hvals * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashValues extends Command { /** * {@inheritdoc} */ public function getId() { return 'HVALS'; } } /** * @link http://redis.io/commands/pfadd * @author Daniele Alessandri <suppakilla@gmail.com> */ class HyperLogLogAdd extends Command { /** * {@inheritdoc} */ public function getId() { return 'PFADD'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeVariadic($arguments); } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/keys * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyKeys extends Command { /** * {@inheritdoc} */ public function getId() { return 'KEYS'; } } /** * @link http://redis.io/commands/move * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyMove extends Command { /** * {@inheritdoc} */ public function getId() { return 'MOVE'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/randomkey * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyRandom extends Command { /** * {@inheritdoc} */ public function getId() { return 'RANDOMKEY'; } /** * {@inheritdoc} */ public function parseResponse($data) { return $data !== '' ? $data : null; } } /** * @link http://redis.io/commands/renamenx * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyRenamePreserve extends KeyRename { /** * {@inheritdoc} */ public function getId() { return 'RENAMENX'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/restore * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyRestore extends Command { /** * {@inheritdoc} */ public function getId() { return 'RESTORE'; } } /** * @link http://redis.io/commands/pttl * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyPreciseTimeToLive extends KeyTimeToLive { /** * {@inheritdoc} */ public function getId() { return 'PTTL'; } } /** * @link http://redis.io/commands/pexpireat * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyPreciseExpireAt extends KeyExpireAt { /** * {@inheritdoc} */ public function getId() { return 'PEXPIREAT'; } } /** * @link http://redis.io/commands/persist * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyPersist extends Command { /** * {@inheritdoc} */ public function getId() { return 'PERSIST'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/pexpire * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyPreciseExpire extends KeyExpire { /** * {@inheritdoc} */ public function getId() { return 'PEXPIRE'; } } /** * @link http://redis.io/commands/hsetnx * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashSetPreserve extends Command { /** * {@inheritdoc} */ public function getId() { return 'HSETNX'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/hmset * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashSetMultiple extends Command { /** * {@inheritdoc} */ public function getId() { return 'HMSET'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { $flattenedKVs = array($arguments[0]); $args = $arguments[1]; foreach ($args as $k => $v) { $flattenedKVs[] = $k; $flattenedKVs[] = $v; } return $flattenedKVs; } return $arguments; } } /** * @link http://redis.io/commands/select * @author Daniele Alessandri <suppakilla@gmail.com> */ class ConnectionSelect extends Command { /** * {@inheritdoc} */ public function getId() { return 'SELECT'; } } /** * @link http://redis.io/commands/hdel * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashDelete extends Command { /** * {@inheritdoc} */ public function getId() { return 'HDEL'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeVariadic($arguments); } } /** * @link http://redis.io/commands/hexists * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashExists extends Command { /** * {@inheritdoc} */ public function getId() { return 'HEXISTS'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/quit * @author Daniele Alessandri <suppakilla@gmail.com> */ class ConnectionQuit extends Command { /** * {@inheritdoc} */ public function getId() { return 'QUIT'; } } /** * @link http://redis.io/commands/ping * @author Daniele Alessandri <suppakilla@gmail.com> */ class ConnectionPing extends Command { /** * {@inheritdoc} */ public function getId() { return 'PING'; } } /** * @link http://redis.io/commands/auth * @author Daniele Alessandri <suppakilla@gmail.com> */ class ConnectionAuth extends Command { /** * {@inheritdoc} */ public function getId() { return 'AUTH'; } } /** * @link http://redis.io/commands/echo * @author Daniele Alessandri <suppakilla@gmail.com> */ class ConnectionEcho extends Command { /** * {@inheritdoc} */ public function getId() { return 'ECHO'; } } /** * @link http://redis.io/commands/hget * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashGet extends Command { /** * {@inheritdoc} */ public function getId() { return 'HGET'; } } /** * @link http://redis.io/commands/hgetall * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashGetAll extends Command { /** * {@inheritdoc} */ public function getId() { return 'HGETALL'; } /** * {@inheritdoc} */ public function parseResponse($data) { $result = array(); for ($i = 0; $i < count($data); $i++) { $result[$data[$i]] = $data[++$i]; } return $result; } } /** * @link http://redis.io/commands/hlen * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashLength extends Command { /** * {@inheritdoc} */ public function getId() { return 'HLEN'; } } /** * @link http://redis.io/commands/hscan * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashScan extends Command { /** * {@inheritdoc} */ public function getId() { return 'HSCAN'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 3 && is_array($arguments[2])) { $options = $this->prepareOptions(array_pop($arguments)); $arguments = array_merge($arguments, $options); } return $arguments; } /** * Returns a list of options and modifiers compatible with Redis. * * @param array $options List of options. * * @return array */ protected function prepareOptions($options) { $options = array_change_key_case($options, CASE_UPPER); $normalized = array(); if (!empty($options['MATCH'])) { $normalized[] = 'MATCH'; $normalized[] = $options['MATCH']; } if (!empty($options['COUNT'])) { $normalized[] = 'COUNT'; $normalized[] = $options['COUNT']; } return $normalized; } /** * {@inheritdoc} */ public function parseResponse($data) { if (is_array($data)) { $fields = $data[1]; $result = array(); for ($i = 0; $i < count($fields); $i++) { $result[$fields[$i]] = $fields[++$i]; } $data[1] = $result; } return $data; } } /** * @link http://redis.io/commands/hset * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashSet extends Command { /** * {@inheritdoc} */ public function getId() { return 'HSET'; } /** * {@inheritdoc} */ public function parseResponse($data) { return (bool) $data; } } /** * @link http://redis.io/commands/hkeys * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashKeys extends Command { /** * {@inheritdoc} */ public function getId() { return 'HKEYS'; } } /** * @link http://redis.io/commands/hincrbyfloat * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashIncrementByFloat extends Command { /** * {@inheritdoc} */ public function getId() { return 'HINCRBYFLOAT'; } } /** * @link http://redis.io/commands/hmget * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashGetMultiple extends Command { /** * {@inheritdoc} */ public function getId() { return 'HMGET'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { return self::normalizeVariadic($arguments); } } /** * @link http://redis.io/commands/hincrby * @author Daniele Alessandri <suppakilla@gmail.com> */ class HashIncrementBy extends Command { /** * {@inheritdoc} */ public function getId() { return 'HINCRBY'; } } /** * @link http://redis.io/commands/scan * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyScan extends Command { /** * {@inheritdoc} */ public function getId() { return 'SCAN'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { $options = $this->prepareOptions(array_pop($arguments)); $arguments = array_merge($arguments, $options); } return $arguments; } /** * Returns a list of options and modifiers compatible with Redis. * * @param array $options List of options. * * @return array */ protected function prepareOptions($options) { $options = array_change_key_case($options, CASE_UPPER); $normalized = array(); if (!empty($options['MATCH'])) { $normalized[] = 'MATCH'; $normalized[] = $options['MATCH']; } if (!empty($options['COUNT'])) { $normalized[] = 'COUNT'; $normalized[] = $options['COUNT']; } return $normalized; } } /** * @link http://redis.io/commands/sort * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeySort extends Command { /** * {@inheritdoc} */ public function getId() { return 'SORT'; } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (count($arguments) === 1) { return $arguments; } $query = array($arguments[0]); $sortParams = array_change_key_case($arguments[1], CASE_UPPER); if (isset($sortParams['BY'])) { $query[] = 'BY'; $query[] = $sortParams['BY']; } if (isset($sortParams['GET'])) { $getargs = $sortParams['GET']; if (is_array($getargs)) { foreach ($getargs as $getarg) { $query[] = 'GET'; $query[] = $getarg; } } else { $query[] = 'GET'; $query[] = $getargs; } } if (isset($sortParams['LIMIT']) && is_array($sortParams['LIMIT']) && count($sortParams['LIMIT']) == 2) { $query[] = 'LIMIT'; $query[] = $sortParams['LIMIT'][0]; $query[] = $sortParams['LIMIT'][1]; } if (isset($sortParams['SORT'])) { $query[] = strtoupper($sortParams['SORT']); } if (isset($sortParams['ALPHA']) && $sortParams['ALPHA'] == true) { $query[] = 'ALPHA'; } if (isset($sortParams['STORE'])) { $query[] = 'STORE'; $query[] = $sortParams['STORE']; } return $query; } } /** * Class for generic "anonymous" Redis commands. * * This command class does not filter input arguments or parse responses, but * can be used to leverage the standard Predis API to execute any command simply * by providing the needed arguments following the command signature as defined * by Redis in its documentation. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RawCommand implements CommandInterface { private $slot; private $commandID; private $arguments; /** * @param array $arguments Command ID and its arguments. * * @throws \InvalidArgumentException */ public function __construct(array $arguments) { if (!$arguments) { throw new InvalidArgumentException( 'The arguments array must contain at least the command ID.' ); } $this->commandID = strtoupper(array_shift($arguments)); $this->arguments = $arguments; } /** * Creates a new raw command using a variadic method. * * @param string $commandID Redis command ID. * @param string ... Arguments list for the command. * * @return CommandInterface */ public static function create($commandID /* [ $arg, ... */) { $arguments = func_get_args(); $command = new self($arguments); return $command; } /** * {@inheritdoc} */ public function getId() { return $this->commandID; } /** * {@inheritdoc} */ public function setArguments(array $arguments) { $this->arguments = $arguments; unset($this->slot); } /** * {@inheritdoc} */ public function setRawArguments(array $arguments) { $this->setArguments($arguments); } /** * {@inheritdoc} */ public function getArguments() { return $this->arguments; } /** * {@inheritdoc} */ public function getArgument($index) { if (isset($this->arguments[$index])) { return $this->arguments[$index]; } } /** * {@inheritdoc} */ public function setSlot($slot) { $this->slot = $slot; } /** * {@inheritdoc} */ public function getSlot() { if (isset($this->slot)) { return $this->slot; } } /** * {@inheritdoc} */ public function parseResponse($data) { return $data; } } /** * Base class used to implement an higher level abstraction for commands based * on Lua scripting with EVAL and EVALSHA. * * @link http://redis.io/commands/eval * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class ScriptCommand extends ServerEvalSHA { /** * Gets the body of a Lua script. * * @return string */ abstract public function getScript(); /** * Specifies the number of arguments that should be considered as keys. * * The default behaviour for the base class is to return 0 to indicate that * all the elements of the arguments array should be considered as keys, but * subclasses can enforce a static number of keys. * * @return int */ protected function getKeysCount() { return 0; } /** * Returns the elements from the arguments that are identified as keys. * * @return array */ public function getKeys() { return array_slice($this->getArguments(), 2, $this->getKeysCount()); } /** * {@inheritdoc} */ protected function filterArguments(array $arguments) { if (($numkeys = $this->getKeysCount()) && $numkeys < 0) { $numkeys = count($arguments) + $numkeys; } return array_merge(array(sha1($this->getScript()), (int) $numkeys), $arguments); } /** * @return array */ public function getEvalArguments() { $arguments = $this->getArguments(); $arguments[0] = $this->getScript(); return $arguments; } } /** * @link http://redis.io/commands/bgrewriteaof * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerBackgroundRewriteAOF extends Command { /** * {@inheritdoc} */ public function getId() { return 'BGREWRITEAOF'; } /** * {@inheritdoc} */ public function parseResponse($data) { return $data == 'Background append only file rewriting started'; } } /** * @link http://redis.io/commands/punsubscribe * @author Daniele Alessandri <suppakilla@gmail.com> */ class PubSubUnsubscribeByPattern extends PubSubUnsubscribe { /** * {@inheritdoc} */ public function getId() { return 'PUNSUBSCRIBE'; } } /** * @link http://redis.io/commands/psubscribe * @author Daniele Alessandri <suppakilla@gmail.com> */ class PubSubSubscribeByPattern extends PubSubSubscribe { /** * {@inheritdoc} */ public function getId() { return 'PSUBSCRIBE'; } } /** * @link http://redis.io/commands/publish * @author Daniele Alessandri <suppakilla@gmail.com> */ class PubSubPublish extends Command { /** * {@inheritdoc} */ public function getId() { return 'PUBLISH'; } } /** * @link http://redis.io/commands/pubsub * @author Daniele Alessandri <suppakilla@gmail.com> */ class PubSubPubsub extends Command { /** * {@inheritdoc} */ public function getId() { return 'PUBSUB'; } /** * {@inheritdoc} */ public function parseResponse($data) { switch (strtolower($this->getArgument(0))) { case 'numsub': return self::processNumsub($data); default: return $data; } } /** * Returns the processed response to PUBSUB NUMSUB. * * @param array $channels List of channels * * @return array */ protected static function processNumsub(array $channels) { $processed = array(); $count = count($channels); for ($i = 0; $i < $count; $i++) { $processed[$channels[$i]] = $channels[++$i]; } return $processed; } } /** * @link http://redis.io/commands/bgsave * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerBackgroundSave extends Command { /** * {@inheritdoc} */ public function getId() { return 'BGSAVE'; } /** * {@inheritdoc} */ public function parseResponse($data) { return $data === 'Background saving started' ? true : $data; } } /** * @link http://redis.io/commands/client-list * @link http://redis.io/commands/client-kill * @link http://redis.io/commands/client-getname * @link http://redis.io/commands/client-setname * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerClient extends Command { /** * {@inheritdoc} */ public function getId() { return 'CLIENT'; } /** * {@inheritdoc} */ public function parseResponse($data) { $args = array_change_key_case($this->getArguments(), CASE_UPPER); switch (strtoupper($args[0])) { case 'LIST': return $this->parseClientList($data); case 'KILL': case 'GETNAME': case 'SETNAME': default: return $data; } } /** * Parses the response to CLIENT LIST and returns a structured list. * * @param string $data Response buffer. * * @return array */ protected function parseClientList($data) { $clients = array(); foreach (explode("\n", $data, -1) as $clientData) { $client = array(); foreach (explode(' ', $clientData) as $kv) { @list($k, $v) = explode('=', $kv); $client[$k] = $v; } $clients[] = $client; } return $clients; } } /** * @link http://redis.io/commands/info * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerInfoV26x extends ServerInfo { /** * {@inheritdoc} */ public function parseResponse($data) { if ($data === '') { return array(); } $info = array(); $current = null; $infoLines = preg_split('/\r?\n/', $data); if (isset($infoLines[0]) && $infoLines[0][0] !== '#') { return parent::parseResponse($data); } foreach ($infoLines as $row) { if ($row === '') { continue; } if (preg_match('/^# (\w+)$/', $row, $matches)) { $info[$matches[1]] = array(); $current = &$info[$matches[1]]; continue; } list($k, $v) = $this->parseRow($row); $current[$k] = $v; } return $info; } } /** * @link http://redis.io/commands/lastsave * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerLastSave extends Command { /** * {@inheritdoc} */ public function getId() { return 'LASTSAVE'; } } /** * @link http://redis.io/commands/monitor * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerMonitor extends Command { /** * {@inheritdoc} */ public function getId() { return 'MONITOR'; } } /** * @link http://redis.io/commands/flushdb * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerFlushDatabase extends Command { /** * {@inheritdoc} */ public function getId() { return 'FLUSHDB'; } } /** * @link http://redis.io/commands/dbsize * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerDatabaseSize extends Command { /** * {@inheritdoc} */ public function getId() { return 'DBSIZE'; } } /** * @link http://redis.io/commands/command * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerCommand extends Command { /** * {@inheritdoc} */ public function getId() { return 'COMMAND'; } } /** * @link http://redis.io/commands/config-set * @link http://redis.io/commands/config-get * @link http://redis.io/commands/config-resetstat * @link http://redis.io/commands/config-rewrite * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerConfig extends Command { /** * {@inheritdoc} */ public function getId() { return 'CONFIG'; } /** * {@inheritdoc} */ public function parseResponse($data) { if (is_array($data)) { $result = array(); for ($i = 0; $i < count($data); $i++) { $result[$data[$i]] = $data[++$i]; } return $result; } return $data; } } /** * Defines a command whose keys can be prefixed. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface PrefixableCommandInterface extends CommandInterface { /** * Prefixes all the keys found in the arguments of the command. * * @param string $prefix String used to prefix the keys. */ public function prefixKeys($prefix); } /** * @link http://redis.io/commands/ltrim * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListTrim extends Command { /** * {@inheritdoc} */ public function getId() { return 'LTRIM'; } } /** * @link http://redis.io/commands/lpop * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPopFirst extends Command { /** * {@inheritdoc} */ public function getId() { return 'LPOP'; } } /** * @link http://redis.io/commands/rpop * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPopLast extends Command { /** * {@inheritdoc} */ public function getId() { return 'RPOP'; } } /** * @link http://redis.io/commands/brpop * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPopLastBlocking extends ListPopFirstBlocking { /** * {@inheritdoc} */ public function getId() { return 'BRPOP'; } } /** * @link http://redis.io/commands/llen * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListLength extends Command { /** * {@inheritdoc} */ public function getId() { return 'LLEN'; } } /** * @link http://redis.io/commands/linsert * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListInsert extends Command { /** * {@inheritdoc} */ public function getId() { return 'LINSERT'; } } /** * @link http://redis.io/commands/type * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyType extends Command { /** * {@inheritdoc} */ public function getId() { return 'TYPE'; } } /** * @link http://redis.io/commands/lindex * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListIndex extends Command { /** * {@inheritdoc} */ public function getId() { return 'LINDEX'; } } /** * @link http://redis.io/commands/rpoplpush * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPopLastPushHead extends Command { /** * {@inheritdoc} */ public function getId() { return 'RPOPLPUSH'; } } /** * @link http://redis.io/commands/brpoplpush * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPopLastPushHeadBlocking extends Command { /** * {@inheritdoc} */ public function getId() { return 'BRPOPLPUSH'; } } /** * @link http://redis.io/commands/lrem * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListRemove extends Command { /** * {@inheritdoc} */ public function getId() { return 'LREM'; } } /** * @link http://redis.io/commands/lset * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListSet extends Command { /** * {@inheritdoc} */ public function getId() { return 'LSET'; } } /** * @link http://redis.io/commands/lrange * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListRange extends Command { /** * {@inheritdoc} */ public function getId() { return 'LRANGE'; } } /** * @link http://redis.io/commands/rpushx * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPushTailX extends Command { /** * {@inheritdoc} */ public function getId() { return 'RPUSHX'; } } /** * @link http://redis.io/commands/lpush * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPushHead extends ListPushTail { /** * {@inheritdoc} */ public function getId() { return 'LPUSH'; } } /** * @link http://redis.io/commands/lpushx * @author Daniele Alessandri <suppakilla@gmail.com> */ class ListPushHeadX extends Command { /** * {@inheritdoc} */ public function getId() { return 'LPUSHX'; } } /** * @link http://redis.io/commands/object * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerObject extends Command { /** * {@inheritdoc} */ public function getId() { return 'OBJECT'; } } /* --------------------------------------------------------------------------- */ namespace Predis\Connection; use InvalidArgumentException; use Predis\CommunicationException; use Predis\Command\CommandInterface; use Predis\Protocol\ProtocolException; use Predis\Protocol\ProtocolProcessorInterface; use Predis\Protocol\Text\ProtocolProcessor as TextProtocolProcessor; use UnexpectedValueException; use ReflectionClass; use Predis\Command\RawCommand; use Predis\NotSupportedException; use Predis\Response\Error as ErrorResponse; use Predis\Response\Status as StatusResponse; /** * Defines a connection object used to communicate with one or multiple * Redis servers. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ConnectionInterface { /** * Opens the connection to Redis. */ public function connect(); /** * Closes the connection to Redis. */ public function disconnect(); /** * Checks if the connection to Redis is considered open. * * @return bool */ public function isConnected(); /** * Writes the request for the given command over the connection. * * @param CommandInterface $command Command instance. */ public function writeRequest(CommandInterface $command); /** * Reads the response to the given command from the connection. * * @param CommandInterface $command Command instance. * * @return mixed */ public function readResponse(CommandInterface $command); /** * Writes a request for the given command over the connection and reads back * the response returned by Redis. * * @param CommandInterface $command Command instance. * * @return mixed */ public function executeCommand(CommandInterface $command); } /** * Defines a connection used to communicate with a single Redis node. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface NodeConnectionInterface extends ConnectionInterface { /** * Returns a string representation of the connection. * * @return string */ public function __toString(); /** * Returns the underlying resource used to communicate with Redis. * * @return mixed */ public function getResource(); /** * Returns the parameters used to initialize the connection. * * @return ParametersInterface */ public function getParameters(); /** * Pushes the given command into a queue of commands executed when * establishing the actual connection to Redis. * * @param CommandInterface $command Instance of a Redis command. */ public function addConnectCommand(CommandInterface $command); /** * Reads a response from the server. * * @return mixed */ public function read(); } /** * Defines a virtual connection composed of multiple connection instances to * single Redis nodes. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface AggregateConnectionInterface extends ConnectionInterface { /** * Adds a connection instance to the aggregate connection. * * @param NodeConnectionInterface $connection Connection instance. */ public function add(NodeConnectionInterface $connection); /** * Removes the specified connection instance from the aggregate connection. * * @param NodeConnectionInterface $connection Connection instance. * * @return bool Returns true if the connection was in the pool. */ public function remove(NodeConnectionInterface $connection); /** * Returns the connection instance in charge for the given command. * * @param CommandInterface $command Command instance. * * @return NodeConnectionInterface */ public function getConnection(CommandInterface $command); /** * Returns a connection instance from the aggregate connection by its alias. * * @param string $connectionID Connection alias. * * @return NodeConnectionInterface|null */ public function getConnectionById($connectionID); } /** * Base class with the common logic used by connection classes to communicate * with Redis. * * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class AbstractConnection implements NodeConnectionInterface { private $resource; private $cachedId; protected $parameters; protected $initCommands = array(); /** * @param ParametersInterface $parameters Initialization parameters for the connection. */ public function __construct(ParametersInterface $parameters) { $this->parameters = $this->assertParameters($parameters); } /** * Disconnects from the server and destroys the underlying resource when * PHP's garbage collector kicks in. */ public function __destruct() { $this->disconnect(); } /** * Checks some of the parameters used to initialize the connection. * * @param ParametersInterface $parameters Initialization parameters for the connection. * * @return ParametersInterface * * @throws \InvalidArgumentException */ protected function assertParameters(ParametersInterface $parameters) { $scheme = $parameters->scheme; if ($scheme !== 'tcp' && $scheme !== 'unix') { throw new InvalidArgumentException("Invalid scheme: '$scheme'."); } if ($scheme === 'unix' && !isset($parameters->path)) { throw new InvalidArgumentException('Missing UNIX domain socket path.'); } return $parameters; } /** * Creates the underlying resource used to communicate with Redis. * * @return mixed */ abstract protected function createResource(); /** * {@inheritdoc} */ public function isConnected() { return isset($this->resource); } /** * {@inheritdoc} */ public function connect() { if (!$this->isConnected()) { $this->resource = $this->createResource(); return true; } return false; } /** * {@inheritdoc} */ public function disconnect() { unset($this->resource); } /** * {@inheritdoc} */ public function addConnectCommand(CommandInterface $command) { $this->initCommands[] = $command; } /** * {@inheritdoc} */ public function executeCommand(CommandInterface $command) { $this->writeRequest($command); return $this->readResponse($command); } /** * {@inheritdoc} */ public function readResponse(CommandInterface $command) { return $this->read(); } /** * Helper method to handle connection errors. * * @param string $message Error message. * @param int $code Error code. */ protected function onConnectionError($message, $code = null) { CommunicationException::handle( new ConnectionException( $this, "$message [{$this->parameters->scheme}://{$this->getIdentifier()}]", $code ) ); } /** * Helper method to handle protocol errors. * * @param string $message Error message. */ protected function onProtocolError($message) { CommunicationException::handle( new ProtocolException( $this, "$message [{$this->parameters->scheme}://{$this->getIdentifier()}]" ) ); } /** * {@inheritdoc} */ public function getResource() { if (isset($this->resource)) { return $this->resource; } $this->connect(); return $this->resource; } /** * {@inheritdoc} */ public function getParameters() { return $this->parameters; } /** * Gets an identifier for the connection. * * @return string */ protected function getIdentifier() { if ($this->parameters->scheme === 'unix') { return $this->parameters->path; } return "{$this->parameters->host}:{$this->parameters->port}"; } /** * {@inheritdoc} */ public function __toString() { if (!isset($this->cachedId)) { $this->cachedId = $this->getIdentifier(); } return $this->cachedId; } /** * {@inheritdoc} */ public function __sleep() { return array('parameters', 'initCommands'); } } /** * Standard connection to Redis servers implemented on top of PHP's streams. * The connection parameters supported by this class are: * * - scheme: it can be either 'tcp' or 'unix'. * - host: hostname or IP address of the server. * - port: TCP port of the server. * - path: path of a UNIX domain socket when scheme is 'unix'. * - timeout: timeout to perform the connection. * - read_write_timeout: timeout of read / write operations. * - async_connect: performs the connection asynchronously. * - tcp_nodelay: enables or disables Nagle's algorithm for coalescing. * - persistent: the connection is left intact after a GC collection. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class StreamConnection extends AbstractConnection { /** * Disconnects from the server and destroys the underlying resource when the * garbage collector kicks in only if the connection has not been marked as * persistent. */ public function __destruct() { if (isset($this->parameters->persistent) && $this->parameters->persistent) { return; } $this->disconnect(); } /** * {@inheritdoc} */ protected function createResource() { $initializer = "{$this->parameters->scheme}StreamInitializer"; $resource = $this->$initializer($this->parameters); return $resource; } /** * Initializes a TCP stream resource. * * @param ParametersInterface $parameters Initialization parameters for the connection. * * @return resource */ protected function tcpStreamInitializer(ParametersInterface $parameters) { $uri = "tcp://{$parameters->host}:{$parameters->port}"; $flags = STREAM_CLIENT_CONNECT; if (isset($parameters->async_connect) && (bool) $parameters->async_connect) { $flags |= STREAM_CLIENT_ASYNC_CONNECT; } if (isset($parameters->persistent) && (bool) $parameters->persistent) { $flags |= STREAM_CLIENT_PERSISTENT; $uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path"; } $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags); if (!$resource) { $this->onConnectionError(trim($errstr), $errno); } if (isset($parameters->read_write_timeout)) { $rwtimeout = (float) $parameters->read_write_timeout; $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; $timeoutSeconds = floor($rwtimeout); $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000; stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds); } if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) { $socket = socket_import_stream($resource); socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay); } return $resource; } /** * Initializes a UNIX stream resource. * * @param ParametersInterface $parameters Initialization parameters for the connection. * * @return resource */ protected function unixStreamInitializer(ParametersInterface $parameters) { $uri = "unix://{$parameters->path}"; $flags = STREAM_CLIENT_CONNECT; if ((bool) $parameters->persistent) { $flags |= STREAM_CLIENT_PERSISTENT; } $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags); if (!$resource) { $this->onConnectionError(trim($errstr), $errno); } if (isset($parameters->read_write_timeout)) { $rwtimeout = (float) $parameters->read_write_timeout; $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; $timeoutSeconds = floor($rwtimeout); $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000; stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds); } return $resource; } /** * {@inheritdoc} */ public function connect() { if (parent::connect() && $this->initCommands) { foreach ($this->initCommands as $command) { $this->executeCommand($command); } } } /** * {@inheritdoc} */ public function disconnect() { if ($this->isConnected()) { fclose($this->getResource()); parent::disconnect(); } } /** * Performs a write operation over the stream of the buffer containing a * command serialized with the Redis wire protocol. * * @param string $buffer Representation of a command in the Redis wire protocol. */ protected function write($buffer) { $socket = $this->getResource(); while (($length = strlen($buffer)) > 0) { $written = @fwrite($socket, $buffer); if ($length === $written) { return; } if ($written === false || $written === 0) { $this->onConnectionError('Error while writing bytes to the server.'); } $buffer = substr($buffer, $written); } } /** * {@inheritdoc} */ public function read() { $socket = $this->getResource(); $chunk = fgets($socket); if ($chunk === false || $chunk === '') { $this->onConnectionError('Error while reading line from the server.'); } $prefix = $chunk[0]; $payload = substr($chunk, 1, -2); switch ($prefix) { case '+': return StatusResponse::get($payload); case '$': $size = (int) $payload; if ($size === -1) { return null; } $bulkData = ''; $bytesLeft = ($size += 2); do { $chunk = fread($socket, min($bytesLeft, 4096)); if ($chunk === false || $chunk === '') { $this->onConnectionError('Error while reading bytes from the server.'); } $bulkData .= $chunk; $bytesLeft = $size - strlen($bulkData); } while ($bytesLeft > 0); return substr($bulkData, 0, -2); case '*': $count = (int) $payload; if ($count === -1) { return null; } $multibulk = array(); for ($i = 0; $i < $count; $i++) { $multibulk[$i] = $this->read(); } return $multibulk; case ':': return (int) $payload; case '-': return new ErrorResponse($payload); default: $this->onProtocolError("Unknown response prefix: '$prefix'."); return; } } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $commandID = $command->getId(); $arguments = $command->getArguments(); $cmdlen = strlen($commandID); $reqlen = count($arguments) + 1; $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n"; for ($i = 0, $reqlen--; $i < $reqlen; $i++) { $argument = $arguments[$i]; $arglen = strlen($argument); $buffer .= "\${$arglen}\r\n{$argument}\r\n"; } $this->write($buffer); } } /** * Interface defining a container for connection parameters. * * The actual list of connection parameters depends on the features supported by * each connection backend class (please refer to their specific documentation), * but the most common parameters used through the library are: * * @property-read string scheme Connection scheme, such as 'tcp' or 'unix'. * @property-read string host IP address or hostname of Redis. * @property-read int port TCP port on which Redis is listening to. * @property-read string path Path of a UNIX domain socket file. * @property-read string alias Alias for the connection. * @property-read float timeout Timeout for the connect() operation. * @property-read float read_write_timeout Timeout for read() and write() operations. * @property-read bool async_connect Performs the connect() operation asynchronously. * @property-read bool tcp_nodelay Toggles the Nagle's algorithm for coalescing. * @property-read bool persistent Leaves the connection open after a GC collection. * @property-read string password Password to access Redis (see the AUTH command). * @property-read string database Database index (see the SELECT command). * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ParametersInterface { /** * Checks if the specified parameters is set. * * @param string $parameter Name of the parameter. * * @return bool */ public function __isset($parameter); /** * Returns the value of the specified parameter. * * @param string $parameter Name of the parameter. * * @return mixed|null */ public function __get($parameter); /** * Returns an array representation of the connection parameters. * * @return array */ public function toArray(); } /** * Interface for classes providing a factory of connections to Redis nodes. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface FactoryInterface { /** * Defines or overrides the connection class identified by a scheme prefix. * * @param string $scheme Target connection scheme. * @param mixed $initializer Fully-qualified name of a class or a callable for lazy initialization. */ public function define($scheme, $initializer); /** * Undefines the connection identified by a scheme prefix. * * @param string $scheme Target connection scheme. */ public function undefine($scheme); /** * Creates a new connection object. * * @param mixed $parameters Initialization parameters for the connection. * * @return NodeConnectionInterface */ public function create($parameters); /** * Aggregates single connections into an aggregate connection instance. * * @param AggregateConnectionInterface $aggregate Aggregate connection instance. * @param array $parameters List of parameters for each connection. */ public function aggregate(AggregateConnectionInterface $aggregate, array $parameters); } /** * Defines a connection to communicate with a single Redis server that leverages * an external protocol processor to handle pluggable protocol handlers. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface CompositeConnectionInterface extends NodeConnectionInterface { /** * Returns the protocol processor used by the connection. */ public function getProtocol(); /** * Writes the buffer containing over the connection. * * @param string $buffer String buffer to be sent over the connection. */ public function writeBuffer($buffer); /** * Reads the given number of bytes from the connection. * * @param int $length Number of bytes to read from the connection. * * @return string */ public function readBuffer($length); /** * Reads a line from the connection. * * @param string */ public function readLine(); } /** * This class provides the implementation of a Predis connection that uses PHP's * streams for network communication and wraps the phpiredis C extension (PHP * bindings for hiredis) to parse and serialize the Redis protocol. * * This class is intended to provide an optional low-overhead alternative for * processing responses from Redis compared to the standard pure-PHP classes. * Differences in speed when dealing with short inline responses are practically * nonexistent, the actual speed boost is for big multibulk responses when this * protocol processor can parse and return responses very fast. * * For instructions on how to build and install the phpiredis extension, please * consult the repository of the project. * * The connection parameters supported by this class are: * * - scheme: it can be either 'tcp' or 'unix'. * - host: hostname or IP address of the server. * - port: TCP port of the server. * - path: path of a UNIX domain socket when scheme is 'unix'. * - timeout: timeout to perform the connection. * - read_write_timeout: timeout of read / write operations. * - async_connect: performs the connection asynchronously. * - tcp_nodelay: enables or disables Nagle's algorithm for coalescing. * - persistent: the connection is left intact after a GC collection. * * @link https://github.com/nrk/phpiredis * @author Daniele Alessandri <suppakilla@gmail.com> */ class PhpiredisStreamConnection extends StreamConnection { private $reader; /** * {@inheritdoc} */ public function __construct(ParametersInterface $parameters) { $this->assertExtensions(); parent::__construct($parameters); $this->reader = $this->createReader(); } /** * {@inheritdoc} */ public function __destruct() { phpiredis_reader_destroy($this->reader); parent::__destruct(); } /** * Checks if the phpiredis extension is loaded in PHP. */ private function assertExtensions() { if (!extension_loaded('phpiredis')) { throw new NotSupportedException( 'The "phpiredis" extension is required by this connection backend.' ); } } /** * {@inheritdoc} */ protected function tcpStreamInitializer(ParametersInterface $parameters) { $uri = "tcp://{$parameters->host}:{$parameters->port}"; $flags = STREAM_CLIENT_CONNECT; $socket = null; if (isset($parameters->async_connect) && (bool) $parameters->async_connect) { $flags |= STREAM_CLIENT_ASYNC_CONNECT; } if (isset($parameters->persistent) && (bool) $parameters->persistent) { $flags |= STREAM_CLIENT_PERSISTENT; $uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path"; } $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags); if (!$resource) { $this->onConnectionError(trim($errstr), $errno); } if (isset($parameters->read_write_timeout) && function_exists('socket_import_stream')) { $rwtimeout = (float) $parameters->read_write_timeout; $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; $timeout = array( 'sec' => $timeoutSeconds = floor($rwtimeout), 'usec' => ($rwtimeout - $timeoutSeconds) * 1000000, ); $socket = $socket ?: socket_import_stream($resource); @socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout); @socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout); } if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) { $socket = $socket ?: socket_import_stream($resource); socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay); } return $resource; } /** * Creates a new instance of the protocol reader resource. * * @return resource */ private function createReader() { $reader = phpiredis_reader_create(); phpiredis_reader_set_status_handler($reader, $this->getStatusHandler()); phpiredis_reader_set_error_handler($reader, $this->getErrorHandler()); return $reader; } /** * Returns the underlying protocol reader resource. * * @return resource */ protected function getReader() { return $this->reader; } /** * Returns the handler used by the protocol reader for inline responses. * * @return \Closure */ protected function getStatusHandler() { return function ($payload) { return StatusResponse::get($payload); }; } /** * Returns the handler used by the protocol reader for error responses. * * @return \Closure */ protected function getErrorHandler() { return function ($errorMessage) { return new ErrorResponse($errorMessage); }; } /** * {@inheritdoc} */ public function read() { $socket = $this->getResource(); $reader = $this->reader; while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) { $buffer = stream_socket_recvfrom($socket, 4096); if ($buffer === false || $buffer === '') { $this->onConnectionError('Error while reading bytes from the server.'); } phpiredis_reader_feed($reader, $buffer); } if ($state === PHPIREDIS_READER_STATE_COMPLETE) { return phpiredis_reader_get_reply($reader); } else { $this->onProtocolError(phpiredis_reader_get_error($reader)); return; } } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $arguments = $command->getArguments(); array_unshift($arguments, $command->getId()); $this->write(phpiredis_format_command($arguments)); } /** * {@inheritdoc} */ public function __wakeup() { $this->assertExtensions(); $this->reader = $this->createReader(); } } /** * This class implements a Predis connection that actually talks with Webdis * instead of connecting directly to Redis. It relies on the cURL extension to * communicate with the web server and the phpiredis extension to parse the * protocol for responses returned in the http response bodies. * * Some features are not yet available or they simply cannot be implemented: * - Pipelining commands. * - Publish / Subscribe. * - MULTI / EXEC transactions (not yet supported by Webdis). * * The connection parameters supported by this class are: * * - scheme: must be 'http'. * - host: hostname or IP address of the server. * - port: TCP port of the server. * - timeout: timeout to perform the connection. * - user: username for authentication. * - pass: password for authentication. * * @link http://webd.is * @link http://github.com/nicolasff/webdis * @link http://github.com/seppo0010/phpiredis * @author Daniele Alessandri <suppakilla@gmail.com> */ class WebdisConnection implements NodeConnectionInterface { private $parameters; private $resource; private $reader; /** * @param ParametersInterface $parameters Initialization parameters for the connection. * * @throws \InvalidArgumentException */ public function __construct(ParametersInterface $parameters) { $this->assertExtensions(); if ($parameters->scheme !== 'http') { throw new InvalidArgumentException("Invalid scheme: '{$parameters->scheme}'."); } $this->parameters = $parameters; $this->resource = $this->createCurl(); $this->reader = $this->createReader(); } /** * Frees the underlying cURL and protocol reader resources when the garbage * collector kicks in. */ public function __destruct() { curl_close($this->resource); phpiredis_reader_destroy($this->reader); } /** * Helper method used to throw on unsupported methods. * * @param string $method Name of the unsupported method. * * @throws NotSupportedException */ private function throwNotSupportedException($method) { $class = __CLASS__; throw new NotSupportedException("The method $class::$method() is not supported."); } /** * Checks if the cURL and phpiredis extensions are loaded in PHP. */ private function assertExtensions() { if (!extension_loaded('curl')) { throw new NotSupportedException( 'The "curl" extension is required by this connection backend.' ); } if (!extension_loaded('phpiredis')) { throw new NotSupportedException( 'The "phpiredis" extension is required by this connection backend.' ); } } /** * Initializes cURL. * * @return resource */ private function createCurl() { $parameters = $this->getParameters(); $options = array( CURLOPT_FAILONERROR => true, CURLOPT_CONNECTTIMEOUT_MS => $parameters->timeout * 1000, CURLOPT_URL => "{$parameters->scheme}://{$parameters->host}:{$parameters->port}", CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_POST => true, CURLOPT_WRITEFUNCTION => array($this, 'feedReader'), ); if (isset($parameters->user, $parameters->pass)) { $options[CURLOPT_USERPWD] = "{$parameters->user}:{$parameters->pass}"; } curl_setopt_array($resource = curl_init(), $options); return $resource; } /** * Initializes the phpiredis protocol reader. * * @return resource */ private function createReader() { $reader = phpiredis_reader_create(); phpiredis_reader_set_status_handler($reader, $this->getStatusHandler()); phpiredis_reader_set_error_handler($reader, $this->getErrorHandler()); return $reader; } /** * Returns the handler used by the protocol reader for inline responses. * * @return \Closure */ protected function getStatusHandler() { return function ($payload) { return StatusResponse::get($payload); }; } /** * Returns the handler used by the protocol reader for error responses. * * @return \Closure */ protected function getErrorHandler() { return function ($payload) { return new ErrorResponse($payload); }; } /** * Feeds the phpredis reader resource with the data read from the network. * * @param resource $resource Reader resource. * @param string $buffer Buffer of data read from a connection. * * @return int */ protected function feedReader($resource, $buffer) { phpiredis_reader_feed($this->reader, $buffer); return strlen($buffer); } /** * {@inheritdoc} */ public function connect() { // NOOP } /** * {@inheritdoc} */ public function disconnect() { // NOOP } /** * {@inheritdoc} */ public function isConnected() { return true; } /** * Checks if the specified command is supported by this connection class. * * @param CommandInterface $command Command instance. * * @return string * * @throws NotSupportedException */ protected function getCommandId(CommandInterface $command) { switch ($commandID = $command->getId()) { case 'AUTH': case 'SELECT': case 'MULTI': case 'EXEC': case 'WATCH': case 'UNWATCH': case 'DISCARD': case 'MONITOR': throw new NotSupportedException("Command '$commandID' is not allowed by Webdis."); default: return $commandID; } } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $this->throwNotSupportedException(__FUNCTION__); } /** * {@inheritdoc} */ public function readResponse(CommandInterface $command) { $this->throwNotSupportedException(__FUNCTION__); } /** * {@inheritdoc} */ public function executeCommand(CommandInterface $command) { $resource = $this->resource; $commandId = $this->getCommandId($command); if ($arguments = $command->getArguments()) { $arguments = implode('/', array_map('urlencode', $arguments)); $serializedCommand = "$commandId/$arguments.raw"; } else { $serializedCommand = "$commandId.raw"; } curl_setopt($resource, CURLOPT_POSTFIELDS, $serializedCommand); if (curl_exec($resource) === false) { $error = curl_error($resource); $errno = curl_errno($resource); throw new ConnectionException($this, trim($error), $errno); } if (phpiredis_reader_get_state($this->reader) !== PHPIREDIS_READER_STATE_COMPLETE) { throw new ProtocolException($this, phpiredis_reader_get_error($this->reader)); } return phpiredis_reader_get_reply($this->reader); } /** * {@inheritdoc} */ public function getResource() { return $this->resource; } /** * {@inheritdoc} */ public function getParameters() { return $this->parameters; } /** * {@inheritdoc} */ public function addConnectCommand(CommandInterface $command) { $this->throwNotSupportedException(__FUNCTION__); } /** * {@inheritdoc} */ public function read() { $this->throwNotSupportedException(__FUNCTION__); } /** * {@inheritdoc} */ public function __toString() { return "{$this->parameters->host}:{$this->parameters->port}"; } /** * {@inheritdoc} */ public function __sleep() { return array('parameters'); } /** * {@inheritdoc} */ public function __wakeup() { $this->assertExtensions(); $this->resource = $this->createCurl(); $this->reader = $this->createReader(); } } /** * This class provides the implementation of a Predis connection that uses the * PHP socket extension for network communication and wraps the phpiredis C * extension (PHP bindings for hiredis) to parse the Redis protocol. * * This class is intended to provide an optional low-overhead alternative for * processing responses from Redis compared to the standard pure-PHP classes. * Differences in speed when dealing with short inline responses are practically * nonexistent, the actual speed boost is for big multibulk responses when this * protocol processor can parse and return responses very fast. * * For instructions on how to build and install the phpiredis extension, please * consult the repository of the project. * * The connection parameters supported by this class are: * * - scheme: it can be either 'tcp' or 'unix'. * - host: hostname or IP address of the server. * - port: TCP port of the server. * - path: path of a UNIX domain socket when scheme is 'unix'. * - timeout: timeout to perform the connection. * - read_write_timeout: timeout of read / write operations. * * @link http://github.com/nrk/phpiredis * @author Daniele Alessandri <suppakilla@gmail.com> */ class PhpiredisSocketConnection extends AbstractConnection { private $reader; /** * {@inheritdoc} */ public function __construct(ParametersInterface $parameters) { $this->assertExtensions(); parent::__construct($parameters); $this->reader = $this->createReader(); } /** * Disconnects from the server and destroys the underlying resource and the * protocol reader resource when PHP's garbage collector kicks in. */ public function __destruct() { phpiredis_reader_destroy($this->reader); parent::__destruct(); } /** * Checks if the socket and phpiredis extensions are loaded in PHP. */ protected function assertExtensions() { if (!extension_loaded('sockets')) { throw new NotSupportedException( 'The "sockets" extension is required by this connection backend.' ); } if (!extension_loaded('phpiredis')) { throw new NotSupportedException( 'The "phpiredis" extension is required by this connection backend.' ); } } /** * {@inheritdoc} */ protected function assertParameters(ParametersInterface $parameters) { if (isset($parameters->persistent)) { throw new NotSupportedException( "Persistent connections are not supported by this connection backend." ); } return parent::assertParameters($parameters); } /** * Creates a new instance of the protocol reader resource. * * @return resource */ private function createReader() { $reader = phpiredis_reader_create(); phpiredis_reader_set_status_handler($reader, $this->getStatusHandler()); phpiredis_reader_set_error_handler($reader, $this->getErrorHandler()); return $reader; } /** * Returns the underlying protocol reader resource. * * @return resource */ protected function getReader() { return $this->reader; } /** * Returns the handler used by the protocol reader for inline responses. * * @return \Closure */ private function getStatusHandler() { return function ($payload) { return StatusResponse::get($payload); }; } /** * Returns the handler used by the protocol reader for error responses. * * @return \Closure */ protected function getErrorHandler() { return function ($payload) { return new ErrorResponse($payload); }; } /** * Helper method used to throw exceptions on socket errors. */ private function emitSocketError() { $errno = socket_last_error(); $errstr = socket_strerror($errno); $this->disconnect(); $this->onConnectionError(trim($errstr), $errno); } /** * {@inheritdoc} */ protected function createResource() { $isUnix = $this->parameters->scheme === 'unix'; $domain = $isUnix ? AF_UNIX : AF_INET; $protocol = $isUnix ? 0 : SOL_TCP; $socket = @call_user_func('socket_create', $domain, SOCK_STREAM, $protocol); if (!is_resource($socket)) { $this->emitSocketError(); } $this->setSocketOptions($socket, $this->parameters); return $socket; } /** * Sets options on the socket resource from the connection parameters. * * @param resource $socket Socket resource. * @param ParametersInterface $parameters Parameters used to initialize the connection. */ private function setSocketOptions($socket, ParametersInterface $parameters) { if ($parameters->scheme !== 'tcp') { return; } if (!socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1)) { $this->emitSocketError(); } if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) { $this->emitSocketError(); } if (isset($parameters->read_write_timeout)) { $rwtimeout = (float) $parameters->read_write_timeout; $timeoutSec = floor($rwtimeout); $timeoutUsec = ($rwtimeout - $timeoutSec) * 1000000; $timeout = array( 'sec' => $timeoutSec, 'usec' => $timeoutUsec, ); if (!socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout)) { $this->emitSocketError(); } if (!socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout)) { $this->emitSocketError(); } } } /** * Gets the address from the connection parameters. * * @param ParametersInterface $parameters Parameters used to initialize the connection. * * @return string */ protected static function getAddress(ParametersInterface $parameters) { if ($parameters->scheme === 'unix') { return $parameters->path; } $host = $parameters->host; if (ip2long($host) === false) { if (false === $addresses = gethostbynamel($host)) { return false; } return $addresses[array_rand($addresses)]; } return $host; } /** * Opens the actual connection to the server with a timeout. * * @param ParametersInterface $parameters Parameters used to initialize the connection. * * @return string */ private function connectWithTimeout(ParametersInterface $parameters) { if (false === $host = self::getAddress($parameters)) { $this->onConnectionError("Cannot resolve the address of '$parameters->host'."); } $socket = $this->getResource(); socket_set_nonblock($socket); if (@socket_connect($socket, $host, (int) $parameters->port) === false) { $error = socket_last_error(); if ($error != SOCKET_EINPROGRESS && $error != SOCKET_EALREADY) { $this->emitSocketError(); } } socket_set_block($socket); $null = null; $selectable = array($socket); $timeout = (float) $parameters->timeout; $timeoutSecs = floor($timeout); $timeoutUSecs = ($timeout - $timeoutSecs) * 1000000; $selected = socket_select($selectable, $selectable, $null, $timeoutSecs, $timeoutUSecs); if ($selected === 2) { $this->onConnectionError('Connection refused.', SOCKET_ECONNREFUSED); } if ($selected === 0) { $this->onConnectionError('Connection timed out.', SOCKET_ETIMEDOUT); } if ($selected === false) { $this->emitSocketError(); } } /** * {@inheritdoc} */ public function connect() { if (parent::connect()) { $this->connectWithTimeout($this->parameters); if ($this->initCommands) { foreach ($this->initCommands as $command) { $this->executeCommand($command); } } } } /** * {@inheritdoc} */ public function disconnect() { if ($this->isConnected()) { socket_close($this->getResource()); parent::disconnect(); } } /** * {@inheritdoc} */ protected function write($buffer) { $socket = $this->getResource(); while (($length = strlen($buffer)) > 0) { $written = socket_write($socket, $buffer, $length); if ($length === $written) { return; } if ($written === false) { $this->onConnectionError('Error while writing bytes to the server.'); } $buffer = substr($buffer, $written); } } /** * {@inheritdoc} */ public function read() { $socket = $this->getResource(); $reader = $this->reader; while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) { if (@socket_recv($socket, $buffer, 4096, 0) === false || $buffer === '') { $this->emitSocketError(); } phpiredis_reader_feed($reader, $buffer); } if ($state === PHPIREDIS_READER_STATE_COMPLETE) { return phpiredis_reader_get_reply($reader); } else { $this->onProtocolError(phpiredis_reader_get_error($reader)); return; } } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $arguments = $command->getArguments(); array_unshift($arguments, $command->getId()); $this->write(phpiredis_format_command($arguments)); } /** * {@inheritdoc} */ public function __wakeup() { $this->assertExtensions(); $this->reader = $this->createReader(); } } /** * Connection abstraction to Redis servers based on PHP's stream that uses an * external protocol processor defining the protocol used for the communication. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class CompositeStreamConnection extends StreamConnection implements CompositeConnectionInterface { protected $protocol; /** * @param ParametersInterface $parameters Initialization parameters for the connection. * @param ProtocolProcessorInterface $protocol Protocol processor. */ public function __construct( ParametersInterface $parameters, ProtocolProcessorInterface $protocol = null ) { $this->parameters = $this->assertParameters($parameters); $this->protocol = $protocol ?: new TextProtocolProcessor(); } /** * {@inheritdoc} */ public function getProtocol() { return $this->protocol; } /** * {@inheritdoc} */ public function writeBuffer($buffer) { $this->write($buffer); } /** * {@inheritdoc} */ public function readBuffer($length) { if ($length <= 0) { throw new InvalidArgumentException('Length parameter must be greater than 0.'); } $value = ''; $socket = $this->getResource(); do { $chunk = fread($socket, $length); if ($chunk === false || $chunk === '') { $this->onConnectionError('Error while reading bytes from the server.'); } $value .= $chunk; } while (($length -= strlen($chunk)) > 0); return $value; } /** * {@inheritdoc} */ public function readLine() { $value = ''; $socket = $this->getResource(); do { $chunk = fgets($socket); if ($chunk === false || $chunk === '') { $this->onConnectionError('Error while reading line from the server.'); } $value .= $chunk; } while (substr($value, -2) !== "\r\n"); return substr($value, 0, -2); } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $this->protocol->write($this, $command); } /** * {@inheritdoc} */ public function read() { return $this->protocol->read($this); } /** * {@inheritdoc} */ public function __sleep() { return array_merge(parent::__sleep(), array('protocol')); } } /** * Exception class that identifies connection-related errors. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ConnectionException extends CommunicationException { } /** * Container for connection parameters used to initialize connections to Redis. * * {@inheritdoc} * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Parameters implements ParametersInterface { private $parameters; private static $defaults = array( 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379, 'timeout' => 5.0, ); /** * @param array $parameters Named array of connection parameters. */ public function __construct(array $parameters = array()) { $this->parameters = $this->filter($parameters) + $this->getDefaults(); } /** * Returns some default parameters with their values. * * @return array */ protected function getDefaults() { return self::$defaults; } /** * Creates a new instance by supplying the initial parameters either in the * form of an URI string or a named array. * * @param array|string $parameters Set of connection parameters. * * @return Parameters */ public static function create($parameters) { if (is_string($parameters)) { $parameters = static::parse($parameters); } return new static($parameters ?: array()); } /** * Parses an URI string returning an array of connection parameters. * * @param string $uri URI string. * * @return array * * @throws \InvalidArgumentException */ public static function parse($uri) { if (stripos($uri, 'unix') === 0) { // Hack to support URIs for UNIX sockets with minimal effort. $uri = str_ireplace('unix:///', 'unix://localhost/', $uri); } if (!($parsed = parse_url($uri)) || !isset($parsed['host'])) { throw new InvalidArgumentException("Invalid parameters URI: $uri"); } if (isset($parsed['query'])) { parse_str($parsed['query'], $queryarray); unset($parsed['query']); $parsed = array_merge($parsed, $queryarray); } return $parsed; } /** * Validates and converts each value of the connection parameters array. * * @param array $parameters Connection parameters. * * @return array */ protected function filter(array $parameters) { return $parameters ?: array(); } /** * {@inheritdoc} */ public function __get($parameter) { if (isset($this->parameters[$parameter])) { return $this->parameters[$parameter]; } } /** * {@inheritdoc} */ public function __isset($parameter) { return isset($this->parameters[$parameter]); } /** * {@inheritdoc} */ public function toArray() { return $this->parameters; } /** * {@inheritdoc} */ public function __sleep() { return array('parameters'); } } /** * Standard connection factory for creating connections to Redis nodes. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Factory implements FactoryInterface { protected $schemes = array( 'tcp' => 'Predis\Connection\StreamConnection', 'unix' => 'Predis\Connection\StreamConnection', 'http' => 'Predis\Connection\WebdisConnection', ); /** * Checks if the provided argument represents a valid connection class * implementing Predis\Connection\NodeConnectionInterface. Optionally, * callable objects are used for lazy initialization of connection objects. * * @param mixed $initializer FQN of a connection class or a callable for lazy initialization. * * @return mixed * * @throws \InvalidArgumentException */ protected function checkInitializer($initializer) { if (is_callable($initializer)) { return $initializer; } $class = new ReflectionClass($initializer); if (!$class->isSubclassOf('Predis\Connection\NodeConnectionInterface')) { throw new InvalidArgumentException( 'A connection initializer must be a valid connection class or a callable object.' ); } return $initializer; } /** * {@inheritdoc} */ public function define($scheme, $initializer) { $this->schemes[$scheme] = $this->checkInitializer($initializer); } /** * {@inheritdoc} */ public function undefine($scheme) { unset($this->schemes[$scheme]); } /** * {@inheritdoc} */ public function create($parameters) { if (!$parameters instanceof ParametersInterface) { $parameters = $this->createParameters($parameters); } $scheme = $parameters->scheme; if (!isset($this->schemes[$scheme])) { throw new InvalidArgumentException("Unknown connection scheme: '$scheme'."); } $initializer = $this->schemes[$scheme]; if (is_callable($initializer)) { $connection = call_user_func($initializer, $parameters, $this); } else { $connection = new $initializer($parameters); $this->prepareConnection($connection); } if (!$connection instanceof NodeConnectionInterface) { throw new UnexpectedValueException( "Objects returned by connection initializers must implement ". "'Predis\Connection\NodeConnectionInterface'." ); } return $connection; } /** * {@inheritdoc} */ public function aggregate(AggregateConnectionInterface $connection, array $parameters) { foreach ($parameters as $node) { $connection->add($node instanceof NodeConnectionInterface ? $node : $this->create($node)); } } /** * Creates a connection parameters instance from the supplied argument. * * @param mixed $parameters Original connection parameters. * * @return ParametersInterface */ protected function createParameters($parameters) { return Parameters::create($parameters); } /** * Prepares a connection instance after its initialization. * * @param NodeConnectionInterface $connection Connection instance. */ protected function prepareConnection(NodeConnectionInterface $connection) { $parameters = $connection->getParameters(); if (isset($parameters->password)) { $connection->addConnectCommand( new RawCommand(array('AUTH', $parameters->password)) ); } if (isset($parameters->database)) { $connection->addConnectCommand( new RawCommand(array('SELECT', $parameters->database)) ); } } } /* --------------------------------------------------------------------------- */ namespace Predis\Profile; use InvalidArgumentException; use ReflectionClass; use Predis\ClientException; use Predis\Command\CommandInterface; use Predis\Command\Processor\ProcessorInterface; /** * A profile defines all the features and commands supported by certain versions * of Redis. Instances of Predis\Client should use a server profile matching the * version of Redis being used. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ProfileInterface { /** * Returns the profile version corresponding to the Redis version. * * @return string */ public function getVersion(); /** * Checks if the profile supports the specified command. * * @param string $commandID Command ID. * * @return bool */ public function supportsCommand($commandID); /** * Checks if the profile supports the specified list of commands. * * @param array $commandIDs List of command IDs. * * @return string */ public function supportsCommands(array $commandIDs); /** * Creates a new command instance. * * @param string $commandID Command ID. * @param array $arguments Arguments for the command. * * @return CommandInterface */ public function createCommand($commandID, array $arguments = array()); } /** * Base class implementing common functionalities for Redis server profiles. * * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class RedisProfile implements ProfileInterface { private $commands; private $processor; /** * */ public function __construct() { $this->commands = $this->getSupportedCommands(); } /** * Returns a map of all the commands supported by the profile and their * actual PHP classes. * * @return array */ abstract protected function getSupportedCommands(); /** * {@inheritdoc} */ public function supportsCommand($commandID) { return isset($this->commands[strtoupper($commandID)]); } /** * {@inheritdoc} */ public function supportsCommands(array $commandIDs) { foreach ($commandIDs as $commandID) { if (!$this->supportsCommand($commandID)) { return false; } } return true; } /** * Returns the fully-qualified name of a class representing the specified * command ID registered in the current server profile. * * @param string $commandID Command ID. * * @return string|null */ public function getCommandClass($commandID) { if (isset($this->commands[$commandID = strtoupper($commandID)])) { return $this->commands[$commandID]; } } /** * {@inheritdoc} */ public function createCommand($commandID, array $arguments = array()) { $commandID = strtoupper($commandID); if (!isset($this->commands[$commandID])) { throw new ClientException("Command '$commandID' is not a registered Redis command."); } $commandClass = $this->commands[$commandID]; $command = new $commandClass(); $command->setArguments($arguments); if (isset($this->processor)) { $this->processor->process($command); } return $command; } /** * Defines a new command in the server profile. * * @param string $commandID Command ID. * @param string $class Fully-qualified name of a Predis\Command\CommandInterface. * * @throws \InvalidArgumentException */ public function defineCommand($commandID, $class) { $reflection = new ReflectionClass($class); if (!$reflection->isSubclassOf('Predis\Command\CommandInterface')) { throw new InvalidArgumentException("The class '$class' is not a valid command class."); } $this->commands[strtoupper($commandID)] = $class; } /** * {@inheritdoc} */ public function setProcessor(ProcessorInterface $processor = null) { $this->processor = $processor; } /** * {@inheritdoc} */ public function getProcessor() { return $this->processor; } /** * Returns the version of server profile as its string representation. * * @return string */ public function __toString() { return $this->getVersion(); } } /** * Server profile for Redis 3.0. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisVersion300 extends RedisProfile { /** * {@inheritdoc} */ public function getVersion() { return '3.0'; } /** * {@inheritdoc} */ public function getSupportedCommands() { return array( /* ---------------- Redis 1.2 ---------------- */ /* commands operating on the key space */ 'EXISTS' => 'Predis\Command\KeyExists', 'DEL' => 'Predis\Command\KeyDelete', 'TYPE' => 'Predis\Command\KeyType', 'KEYS' => 'Predis\Command\KeyKeys', 'RANDOMKEY' => 'Predis\Command\KeyRandom', 'RENAME' => 'Predis\Command\KeyRename', 'RENAMENX' => 'Predis\Command\KeyRenamePreserve', 'EXPIRE' => 'Predis\Command\KeyExpire', 'EXPIREAT' => 'Predis\Command\KeyExpireAt', 'TTL' => 'Predis\Command\KeyTimeToLive', 'MOVE' => 'Predis\Command\KeyMove', 'SORT' => 'Predis\Command\KeySort', 'DUMP' => 'Predis\Command\KeyDump', 'RESTORE' => 'Predis\Command\KeyRestore', /* commands operating on string values */ 'SET' => 'Predis\Command\StringSet', 'SETNX' => 'Predis\Command\StringSetPreserve', 'MSET' => 'Predis\Command\StringSetMultiple', 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve', 'GET' => 'Predis\Command\StringGet', 'MGET' => 'Predis\Command\StringGetMultiple', 'GETSET' => 'Predis\Command\StringGetSet', 'INCR' => 'Predis\Command\StringIncrement', 'INCRBY' => 'Predis\Command\StringIncrementBy', 'DECR' => 'Predis\Command\StringDecrement', 'DECRBY' => 'Predis\Command\StringDecrementBy', /* commands operating on lists */ 'RPUSH' => 'Predis\Command\ListPushTail', 'LPUSH' => 'Predis\Command\ListPushHead', 'LLEN' => 'Predis\Command\ListLength', 'LRANGE' => 'Predis\Command\ListRange', 'LTRIM' => 'Predis\Command\ListTrim', 'LINDEX' => 'Predis\Command\ListIndex', 'LSET' => 'Predis\Command\ListSet', 'LREM' => 'Predis\Command\ListRemove', 'LPOP' => 'Predis\Command\ListPopFirst', 'RPOP' => 'Predis\Command\ListPopLast', 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead', /* commands operating on sets */ 'SADD' => 'Predis\Command\SetAdd', 'SREM' => 'Predis\Command\SetRemove', 'SPOP' => 'Predis\Command\SetPop', 'SMOVE' => 'Predis\Command\SetMove', 'SCARD' => 'Predis\Command\SetCardinality', 'SISMEMBER' => 'Predis\Command\SetIsMember', 'SINTER' => 'Predis\Command\SetIntersection', 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore', 'SUNION' => 'Predis\Command\SetUnion', 'SUNIONSTORE' => 'Predis\Command\SetUnionStore', 'SDIFF' => 'Predis\Command\SetDifference', 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore', 'SMEMBERS' => 'Predis\Command\SetMembers', 'SRANDMEMBER' => 'Predis\Command\SetRandomMember', /* commands operating on sorted sets */ 'ZADD' => 'Predis\Command\ZSetAdd', 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy', 'ZREM' => 'Predis\Command\ZSetRemove', 'ZRANGE' => 'Predis\Command\ZSetRange', 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange', 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore', 'ZCARD' => 'Predis\Command\ZSetCardinality', 'ZSCORE' => 'Predis\Command\ZSetScore', 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore', /* connection related commands */ 'PING' => 'Predis\Command\ConnectionPing', 'AUTH' => 'Predis\Command\ConnectionAuth', 'SELECT' => 'Predis\Command\ConnectionSelect', 'ECHO' => 'Predis\Command\ConnectionEcho', 'QUIT' => 'Predis\Command\ConnectionQuit', /* remote server control commands */ 'INFO' => 'Predis\Command\ServerInfoV26x', 'SLAVEOF' => 'Predis\Command\ServerSlaveOf', 'MONITOR' => 'Predis\Command\ServerMonitor', 'DBSIZE' => 'Predis\Command\ServerDatabaseSize', 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase', 'FLUSHALL' => 'Predis\Command\ServerFlushAll', 'SAVE' => 'Predis\Command\ServerSave', 'BGSAVE' => 'Predis\Command\ServerBackgroundSave', 'LASTSAVE' => 'Predis\Command\ServerLastSave', 'SHUTDOWN' => 'Predis\Command\ServerShutdown', 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF', /* ---------------- Redis 2.0 ---------------- */ /* commands operating on string values */ 'SETEX' => 'Predis\Command\StringSetExpire', 'APPEND' => 'Predis\Command\StringAppend', 'SUBSTR' => 'Predis\Command\StringSubstr', /* commands operating on lists */ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking', 'BRPOP' => 'Predis\Command\ListPopLastBlocking', /* commands operating on sorted sets */ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore', 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore', 'ZCOUNT' => 'Predis\Command\ZSetCount', 'ZRANK' => 'Predis\Command\ZSetRank', 'ZREVRANK' => 'Predis\Command\ZSetReverseRank', 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank', /* commands operating on hashes */ 'HSET' => 'Predis\Command\HashSet', 'HSETNX' => 'Predis\Command\HashSetPreserve', 'HMSET' => 'Predis\Command\HashSetMultiple', 'HINCRBY' => 'Predis\Command\HashIncrementBy', 'HGET' => 'Predis\Command\HashGet', 'HMGET' => 'Predis\Command\HashGetMultiple', 'HDEL' => 'Predis\Command\HashDelete', 'HEXISTS' => 'Predis\Command\HashExists', 'HLEN' => 'Predis\Command\HashLength', 'HKEYS' => 'Predis\Command\HashKeys', 'HVALS' => 'Predis\Command\HashValues', 'HGETALL' => 'Predis\Command\HashGetAll', /* transactions */ 'MULTI' => 'Predis\Command\TransactionMulti', 'EXEC' => 'Predis\Command\TransactionExec', 'DISCARD' => 'Predis\Command\TransactionDiscard', /* publish - subscribe */ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe', 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe', 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern', 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern', 'PUBLISH' => 'Predis\Command\PubSubPublish', /* remote server control commands */ 'CONFIG' => 'Predis\Command\ServerConfig', /* ---------------- Redis 2.2 ---------------- */ /* commands operating on the key space */ 'PERSIST' => 'Predis\Command\KeyPersist', /* commands operating on string values */ 'STRLEN' => 'Predis\Command\StringStrlen', 'SETRANGE' => 'Predis\Command\StringSetRange', 'GETRANGE' => 'Predis\Command\StringGetRange', 'SETBIT' => 'Predis\Command\StringSetBit', 'GETBIT' => 'Predis\Command\StringGetBit', /* commands operating on lists */ 'RPUSHX' => 'Predis\Command\ListPushTailX', 'LPUSHX' => 'Predis\Command\ListPushHeadX', 'LINSERT' => 'Predis\Command\ListInsert', 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking', /* commands operating on sorted sets */ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore', /* transactions */ 'WATCH' => 'Predis\Command\TransactionWatch', 'UNWATCH' => 'Predis\Command\TransactionUnwatch', /* remote server control commands */ 'OBJECT' => 'Predis\Command\ServerObject', 'SLOWLOG' => 'Predis\Command\ServerSlowlog', /* ---------------- Redis 2.4 ---------------- */ /* remote server control commands */ 'CLIENT' => 'Predis\Command\ServerClient', /* ---------------- Redis 2.6 ---------------- */ /* commands operating on the key space */ 'PTTL' => 'Predis\Command\KeyPreciseTimeToLive', 'PEXPIRE' => 'Predis\Command\KeyPreciseExpire', 'PEXPIREAT' => 'Predis\Command\KeyPreciseExpireAt', /* commands operating on string values */ 'PSETEX' => 'Predis\Command\StringPreciseSetExpire', 'INCRBYFLOAT' => 'Predis\Command\StringIncrementByFloat', 'BITOP' => 'Predis\Command\StringBitOp', 'BITCOUNT' => 'Predis\Command\StringBitCount', /* commands operating on hashes */ 'HINCRBYFLOAT' => 'Predis\Command\HashIncrementByFloat', /* scripting */ 'EVAL' => 'Predis\Command\ServerEval', 'EVALSHA' => 'Predis\Command\ServerEvalSHA', 'SCRIPT' => 'Predis\Command\ServerScript', /* remote server control commands */ 'TIME' => 'Predis\Command\ServerTime', 'SENTINEL' => 'Predis\Command\ServerSentinel', /* ---------------- Redis 2.8 ---------------- */ /* commands operating on the key space */ 'SCAN' => 'Predis\Command\KeyScan', /* commands operating on string values */ 'BITPOS' => 'Predis\Command\StringBitPos', /* commands operating on sets */ 'SSCAN' => 'Predis\Command\SetScan', /* commands operating on sorted sets */ 'ZSCAN' => 'Predis\Command\ZSetScan', 'ZLEXCOUNT' => 'Predis\Command\ZSetLexCount', 'ZRANGEBYLEX' => 'Predis\Command\ZSetRangeByLex', 'ZREMRANGEBYLEX' => 'Predis\Command\ZSetRemoveRangeByLex', /* commands operating on hashes */ 'HSCAN' => 'Predis\Command\HashScan', /* publish - subscribe */ 'PUBSUB' => 'Predis\Command\PubSubPubsub', /* commands operating on HyperLogLog */ 'PFADD' => 'Predis\Command\HyperLogLogAdd', 'PFCOUNT' => 'Predis\Command\HyperLogLogCount', 'PFMERGE' => 'Predis\Command\HyperLogLogMerge', /* remote server control commands */ 'COMMAND' => 'Predis\Command\ServerCommand', /* ---------------- Redis 3.0 ---------------- */ ); } } /** * Server profile for Redis 2.6. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisVersion260 extends RedisProfile { /** * {@inheritdoc} */ public function getVersion() { return '2.6'; } /** * {@inheritdoc} */ public function getSupportedCommands() { return array( /* ---------------- Redis 1.2 ---------------- */ /* commands operating on the key space */ 'EXISTS' => 'Predis\Command\KeyExists', 'DEL' => 'Predis\Command\KeyDelete', 'TYPE' => 'Predis\Command\KeyType', 'KEYS' => 'Predis\Command\KeyKeys', 'RANDOMKEY' => 'Predis\Command\KeyRandom', 'RENAME' => 'Predis\Command\KeyRename', 'RENAMENX' => 'Predis\Command\KeyRenamePreserve', 'EXPIRE' => 'Predis\Command\KeyExpire', 'EXPIREAT' => 'Predis\Command\KeyExpireAt', 'TTL' => 'Predis\Command\KeyTimeToLive', 'MOVE' => 'Predis\Command\KeyMove', 'SORT' => 'Predis\Command\KeySort', 'DUMP' => 'Predis\Command\KeyDump', 'RESTORE' => 'Predis\Command\KeyRestore', /* commands operating on string values */ 'SET' => 'Predis\Command\StringSet', 'SETNX' => 'Predis\Command\StringSetPreserve', 'MSET' => 'Predis\Command\StringSetMultiple', 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve', 'GET' => 'Predis\Command\StringGet', 'MGET' => 'Predis\Command\StringGetMultiple', 'GETSET' => 'Predis\Command\StringGetSet', 'INCR' => 'Predis\Command\StringIncrement', 'INCRBY' => 'Predis\Command\StringIncrementBy', 'DECR' => 'Predis\Command\StringDecrement', 'DECRBY' => 'Predis\Command\StringDecrementBy', /* commands operating on lists */ 'RPUSH' => 'Predis\Command\ListPushTail', 'LPUSH' => 'Predis\Command\ListPushHead', 'LLEN' => 'Predis\Command\ListLength', 'LRANGE' => 'Predis\Command\ListRange', 'LTRIM' => 'Predis\Command\ListTrim', 'LINDEX' => 'Predis\Command\ListIndex', 'LSET' => 'Predis\Command\ListSet', 'LREM' => 'Predis\Command\ListRemove', 'LPOP' => 'Predis\Command\ListPopFirst', 'RPOP' => 'Predis\Command\ListPopLast', 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead', /* commands operating on sets */ 'SADD' => 'Predis\Command\SetAdd', 'SREM' => 'Predis\Command\SetRemove', 'SPOP' => 'Predis\Command\SetPop', 'SMOVE' => 'Predis\Command\SetMove', 'SCARD' => 'Predis\Command\SetCardinality', 'SISMEMBER' => 'Predis\Command\SetIsMember', 'SINTER' => 'Predis\Command\SetIntersection', 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore', 'SUNION' => 'Predis\Command\SetUnion', 'SUNIONSTORE' => 'Predis\Command\SetUnionStore', 'SDIFF' => 'Predis\Command\SetDifference', 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore', 'SMEMBERS' => 'Predis\Command\SetMembers', 'SRANDMEMBER' => 'Predis\Command\SetRandomMember', /* commands operating on sorted sets */ 'ZADD' => 'Predis\Command\ZSetAdd', 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy', 'ZREM' => 'Predis\Command\ZSetRemove', 'ZRANGE' => 'Predis\Command\ZSetRange', 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange', 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore', 'ZCARD' => 'Predis\Command\ZSetCardinality', 'ZSCORE' => 'Predis\Command\ZSetScore', 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore', /* connection related commands */ 'PING' => 'Predis\Command\ConnectionPing', 'AUTH' => 'Predis\Command\ConnectionAuth', 'SELECT' => 'Predis\Command\ConnectionSelect', 'ECHO' => 'Predis\Command\ConnectionEcho', 'QUIT' => 'Predis\Command\ConnectionQuit', /* remote server control commands */ 'INFO' => 'Predis\Command\ServerInfoV26x', 'SLAVEOF' => 'Predis\Command\ServerSlaveOf', 'MONITOR' => 'Predis\Command\ServerMonitor', 'DBSIZE' => 'Predis\Command\ServerDatabaseSize', 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase', 'FLUSHALL' => 'Predis\Command\ServerFlushAll', 'SAVE' => 'Predis\Command\ServerSave', 'BGSAVE' => 'Predis\Command\ServerBackgroundSave', 'LASTSAVE' => 'Predis\Command\ServerLastSave', 'SHUTDOWN' => 'Predis\Command\ServerShutdown', 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF', /* ---------------- Redis 2.0 ---------------- */ /* commands operating on string values */ 'SETEX' => 'Predis\Command\StringSetExpire', 'APPEND' => 'Predis\Command\StringAppend', 'SUBSTR' => 'Predis\Command\StringSubstr', /* commands operating on lists */ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking', 'BRPOP' => 'Predis\Command\ListPopLastBlocking', /* commands operating on sorted sets */ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore', 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore', 'ZCOUNT' => 'Predis\Command\ZSetCount', 'ZRANK' => 'Predis\Command\ZSetRank', 'ZREVRANK' => 'Predis\Command\ZSetReverseRank', 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank', /* commands operating on hashes */ 'HSET' => 'Predis\Command\HashSet', 'HSETNX' => 'Predis\Command\HashSetPreserve', 'HMSET' => 'Predis\Command\HashSetMultiple', 'HINCRBY' => 'Predis\Command\HashIncrementBy', 'HGET' => 'Predis\Command\HashGet', 'HMGET' => 'Predis\Command\HashGetMultiple', 'HDEL' => 'Predis\Command\HashDelete', 'HEXISTS' => 'Predis\Command\HashExists', 'HLEN' => 'Predis\Command\HashLength', 'HKEYS' => 'Predis\Command\HashKeys', 'HVALS' => 'Predis\Command\HashValues', 'HGETALL' => 'Predis\Command\HashGetAll', /* transactions */ 'MULTI' => 'Predis\Command\TransactionMulti', 'EXEC' => 'Predis\Command\TransactionExec', 'DISCARD' => 'Predis\Command\TransactionDiscard', /* publish - subscribe */ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe', 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe', 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern', 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern', 'PUBLISH' => 'Predis\Command\PubSubPublish', /* remote server control commands */ 'CONFIG' => 'Predis\Command\ServerConfig', /* ---------------- Redis 2.2 ---------------- */ /* commands operating on the key space */ 'PERSIST' => 'Predis\Command\KeyPersist', /* commands operating on string values */ 'STRLEN' => 'Predis\Command\StringStrlen', 'SETRANGE' => 'Predis\Command\StringSetRange', 'GETRANGE' => 'Predis\Command\StringGetRange', 'SETBIT' => 'Predis\Command\StringSetBit', 'GETBIT' => 'Predis\Command\StringGetBit', /* commands operating on lists */ 'RPUSHX' => 'Predis\Command\ListPushTailX', 'LPUSHX' => 'Predis\Command\ListPushHeadX', 'LINSERT' => 'Predis\Command\ListInsert', 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking', /* commands operating on sorted sets */ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore', /* transactions */ 'WATCH' => 'Predis\Command\TransactionWatch', 'UNWATCH' => 'Predis\Command\TransactionUnwatch', /* remote server control commands */ 'OBJECT' => 'Predis\Command\ServerObject', 'SLOWLOG' => 'Predis\Command\ServerSlowlog', /* ---------------- Redis 2.4 ---------------- */ /* remote server control commands */ 'CLIENT' => 'Predis\Command\ServerClient', /* ---------------- Redis 2.6 ---------------- */ /* commands operating on the key space */ 'PTTL' => 'Predis\Command\KeyPreciseTimeToLive', 'PEXPIRE' => 'Predis\Command\KeyPreciseExpire', 'PEXPIREAT' => 'Predis\Command\KeyPreciseExpireAt', /* commands operating on string values */ 'PSETEX' => 'Predis\Command\StringPreciseSetExpire', 'INCRBYFLOAT' => 'Predis\Command\StringIncrementByFloat', 'BITOP' => 'Predis\Command\StringBitOp', 'BITCOUNT' => 'Predis\Command\StringBitCount', /* commands operating on hashes */ 'HINCRBYFLOAT' => 'Predis\Command\HashIncrementByFloat', /* scripting */ 'EVAL' => 'Predis\Command\ServerEval', 'EVALSHA' => 'Predis\Command\ServerEvalSHA', 'SCRIPT' => 'Predis\Command\ServerScript', /* remote server control commands */ 'TIME' => 'Predis\Command\ServerTime', 'SENTINEL' => 'Predis\Command\ServerSentinel', ); } } /** * Server profile for Redis 2.8. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisVersion280 extends RedisProfile { /** * {@inheritdoc} */ public function getVersion() { return '2.8'; } /** * {@inheritdoc} */ public function getSupportedCommands() { return array( /* ---------------- Redis 1.2 ---------------- */ /* commands operating on the key space */ 'EXISTS' => 'Predis\Command\KeyExists', 'DEL' => 'Predis\Command\KeyDelete', 'TYPE' => 'Predis\Command\KeyType', 'KEYS' => 'Predis\Command\KeyKeys', 'RANDOMKEY' => 'Predis\Command\KeyRandom', 'RENAME' => 'Predis\Command\KeyRename', 'RENAMENX' => 'Predis\Command\KeyRenamePreserve', 'EXPIRE' => 'Predis\Command\KeyExpire', 'EXPIREAT' => 'Predis\Command\KeyExpireAt', 'TTL' => 'Predis\Command\KeyTimeToLive', 'MOVE' => 'Predis\Command\KeyMove', 'SORT' => 'Predis\Command\KeySort', 'DUMP' => 'Predis\Command\KeyDump', 'RESTORE' => 'Predis\Command\KeyRestore', /* commands operating on string values */ 'SET' => 'Predis\Command\StringSet', 'SETNX' => 'Predis\Command\StringSetPreserve', 'MSET' => 'Predis\Command\StringSetMultiple', 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve', 'GET' => 'Predis\Command\StringGet', 'MGET' => 'Predis\Command\StringGetMultiple', 'GETSET' => 'Predis\Command\StringGetSet', 'INCR' => 'Predis\Command\StringIncrement', 'INCRBY' => 'Predis\Command\StringIncrementBy', 'DECR' => 'Predis\Command\StringDecrement', 'DECRBY' => 'Predis\Command\StringDecrementBy', /* commands operating on lists */ 'RPUSH' => 'Predis\Command\ListPushTail', 'LPUSH' => 'Predis\Command\ListPushHead', 'LLEN' => 'Predis\Command\ListLength', 'LRANGE' => 'Predis\Command\ListRange', 'LTRIM' => 'Predis\Command\ListTrim', 'LINDEX' => 'Predis\Command\ListIndex', 'LSET' => 'Predis\Command\ListSet', 'LREM' => 'Predis\Command\ListRemove', 'LPOP' => 'Predis\Command\ListPopFirst', 'RPOP' => 'Predis\Command\ListPopLast', 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead', /* commands operating on sets */ 'SADD' => 'Predis\Command\SetAdd', 'SREM' => 'Predis\Command\SetRemove', 'SPOP' => 'Predis\Command\SetPop', 'SMOVE' => 'Predis\Command\SetMove', 'SCARD' => 'Predis\Command\SetCardinality', 'SISMEMBER' => 'Predis\Command\SetIsMember', 'SINTER' => 'Predis\Command\SetIntersection', 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore', 'SUNION' => 'Predis\Command\SetUnion', 'SUNIONSTORE' => 'Predis\Command\SetUnionStore', 'SDIFF' => 'Predis\Command\SetDifference', 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore', 'SMEMBERS' => 'Predis\Command\SetMembers', 'SRANDMEMBER' => 'Predis\Command\SetRandomMember', /* commands operating on sorted sets */ 'ZADD' => 'Predis\Command\ZSetAdd', 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy', 'ZREM' => 'Predis\Command\ZSetRemove', 'ZRANGE' => 'Predis\Command\ZSetRange', 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange', 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore', 'ZCARD' => 'Predis\Command\ZSetCardinality', 'ZSCORE' => 'Predis\Command\ZSetScore', 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore', /* connection related commands */ 'PING' => 'Predis\Command\ConnectionPing', 'AUTH' => 'Predis\Command\ConnectionAuth', 'SELECT' => 'Predis\Command\ConnectionSelect', 'ECHO' => 'Predis\Command\ConnectionEcho', 'QUIT' => 'Predis\Command\ConnectionQuit', /* remote server control commands */ 'INFO' => 'Predis\Command\ServerInfoV26x', 'SLAVEOF' => 'Predis\Command\ServerSlaveOf', 'MONITOR' => 'Predis\Command\ServerMonitor', 'DBSIZE' => 'Predis\Command\ServerDatabaseSize', 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase', 'FLUSHALL' => 'Predis\Command\ServerFlushAll', 'SAVE' => 'Predis\Command\ServerSave', 'BGSAVE' => 'Predis\Command\ServerBackgroundSave', 'LASTSAVE' => 'Predis\Command\ServerLastSave', 'SHUTDOWN' => 'Predis\Command\ServerShutdown', 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF', /* ---------------- Redis 2.0 ---------------- */ /* commands operating on string values */ 'SETEX' => 'Predis\Command\StringSetExpire', 'APPEND' => 'Predis\Command\StringAppend', 'SUBSTR' => 'Predis\Command\StringSubstr', /* commands operating on lists */ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking', 'BRPOP' => 'Predis\Command\ListPopLastBlocking', /* commands operating on sorted sets */ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore', 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore', 'ZCOUNT' => 'Predis\Command\ZSetCount', 'ZRANK' => 'Predis\Command\ZSetRank', 'ZREVRANK' => 'Predis\Command\ZSetReverseRank', 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank', /* commands operating on hashes */ 'HSET' => 'Predis\Command\HashSet', 'HSETNX' => 'Predis\Command\HashSetPreserve', 'HMSET' => 'Predis\Command\HashSetMultiple', 'HINCRBY' => 'Predis\Command\HashIncrementBy', 'HGET' => 'Predis\Command\HashGet', 'HMGET' => 'Predis\Command\HashGetMultiple', 'HDEL' => 'Predis\Command\HashDelete', 'HEXISTS' => 'Predis\Command\HashExists', 'HLEN' => 'Predis\Command\HashLength', 'HKEYS' => 'Predis\Command\HashKeys', 'HVALS' => 'Predis\Command\HashValues', 'HGETALL' => 'Predis\Command\HashGetAll', /* transactions */ 'MULTI' => 'Predis\Command\TransactionMulti', 'EXEC' => 'Predis\Command\TransactionExec', 'DISCARD' => 'Predis\Command\TransactionDiscard', /* publish - subscribe */ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe', 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe', 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern', 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern', 'PUBLISH' => 'Predis\Command\PubSubPublish', /* remote server control commands */ 'CONFIG' => 'Predis\Command\ServerConfig', /* ---------------- Redis 2.2 ---------------- */ /* commands operating on the key space */ 'PERSIST' => 'Predis\Command\KeyPersist', /* commands operating on string values */ 'STRLEN' => 'Predis\Command\StringStrlen', 'SETRANGE' => 'Predis\Command\StringSetRange', 'GETRANGE' => 'Predis\Command\StringGetRange', 'SETBIT' => 'Predis\Command\StringSetBit', 'GETBIT' => 'Predis\Command\StringGetBit', /* commands operating on lists */ 'RPUSHX' => 'Predis\Command\ListPushTailX', 'LPUSHX' => 'Predis\Command\ListPushHeadX', 'LINSERT' => 'Predis\Command\ListInsert', 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking', /* commands operating on sorted sets */ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore', /* transactions */ 'WATCH' => 'Predis\Command\TransactionWatch', 'UNWATCH' => 'Predis\Command\TransactionUnwatch', /* remote server control commands */ 'OBJECT' => 'Predis\Command\ServerObject', 'SLOWLOG' => 'Predis\Command\ServerSlowlog', /* ---------------- Redis 2.4 ---------------- */ /* remote server control commands */ 'CLIENT' => 'Predis\Command\ServerClient', /* ---------------- Redis 2.6 ---------------- */ /* commands operating on the key space */ 'PTTL' => 'Predis\Command\KeyPreciseTimeToLive', 'PEXPIRE' => 'Predis\Command\KeyPreciseExpire', 'PEXPIREAT' => 'Predis\Command\KeyPreciseExpireAt', /* commands operating on string values */ 'PSETEX' => 'Predis\Command\StringPreciseSetExpire', 'INCRBYFLOAT' => 'Predis\Command\StringIncrementByFloat', 'BITOP' => 'Predis\Command\StringBitOp', 'BITCOUNT' => 'Predis\Command\StringBitCount', /* commands operating on hashes */ 'HINCRBYFLOAT' => 'Predis\Command\HashIncrementByFloat', /* scripting */ 'EVAL' => 'Predis\Command\ServerEval', 'EVALSHA' => 'Predis\Command\ServerEvalSHA', 'SCRIPT' => 'Predis\Command\ServerScript', /* remote server control commands */ 'TIME' => 'Predis\Command\ServerTime', 'SENTINEL' => 'Predis\Command\ServerSentinel', /* ---------------- Redis 2.8 ---------------- */ /* commands operating on the key space */ 'SCAN' => 'Predis\Command\KeyScan', /* commands operating on string values */ 'BITPOS' => 'Predis\Command\StringBitPos', /* commands operating on sets */ 'SSCAN' => 'Predis\Command\SetScan', /* commands operating on sorted sets */ 'ZSCAN' => 'Predis\Command\ZSetScan', 'ZLEXCOUNT' => 'Predis\Command\ZSetLexCount', 'ZRANGEBYLEX' => 'Predis\Command\ZSetRangeByLex', 'ZREMRANGEBYLEX' => 'Predis\Command\ZSetRemoveRangeByLex', /* commands operating on hashes */ 'HSCAN' => 'Predis\Command\HashScan', /* publish - subscribe */ 'PUBSUB' => 'Predis\Command\PubSubPubsub', /* commands operating on HyperLogLog */ 'PFADD' => 'Predis\Command\HyperLogLogAdd', 'PFCOUNT' => 'Predis\Command\HyperLogLogCount', 'PFMERGE' => 'Predis\Command\HyperLogLogMerge', /* remote server control commands */ 'COMMAND' => 'Predis\Command\ServerCommand', ); } } /** * Server profile for Redis 2.4. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisVersion240 extends RedisProfile { /** * {@inheritdoc} */ public function getVersion() { return '2.4'; } /** * {@inheritdoc} */ public function getSupportedCommands() { return array( /* ---------------- Redis 1.2 ---------------- */ /* commands operating on the key space */ 'EXISTS' => 'Predis\Command\KeyExists', 'DEL' => 'Predis\Command\KeyDelete', 'TYPE' => 'Predis\Command\KeyType', 'KEYS' => 'Predis\Command\KeyKeys', 'RANDOMKEY' => 'Predis\Command\KeyRandom', 'RENAME' => 'Predis\Command\KeyRename', 'RENAMENX' => 'Predis\Command\KeyRenamePreserve', 'EXPIRE' => 'Predis\Command\KeyExpire', 'EXPIREAT' => 'Predis\Command\KeyExpireAt', 'TTL' => 'Predis\Command\KeyTimeToLive', 'MOVE' => 'Predis\Command\KeyMove', 'SORT' => 'Predis\Command\KeySort', /* commands operating on string values */ 'SET' => 'Predis\Command\StringSet', 'SETNX' => 'Predis\Command\StringSetPreserve', 'MSET' => 'Predis\Command\StringSetMultiple', 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve', 'GET' => 'Predis\Command\StringGet', 'MGET' => 'Predis\Command\StringGetMultiple', 'GETSET' => 'Predis\Command\StringGetSet', 'INCR' => 'Predis\Command\StringIncrement', 'INCRBY' => 'Predis\Command\StringIncrementBy', 'DECR' => 'Predis\Command\StringDecrement', 'DECRBY' => 'Predis\Command\StringDecrementBy', /* commands operating on lists */ 'RPUSH' => 'Predis\Command\ListPushTail', 'LPUSH' => 'Predis\Command\ListPushHead', 'LLEN' => 'Predis\Command\ListLength', 'LRANGE' => 'Predis\Command\ListRange', 'LTRIM' => 'Predis\Command\ListTrim', 'LINDEX' => 'Predis\Command\ListIndex', 'LSET' => 'Predis\Command\ListSet', 'LREM' => 'Predis\Command\ListRemove', 'LPOP' => 'Predis\Command\ListPopFirst', 'RPOP' => 'Predis\Command\ListPopLast', 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead', /* commands operating on sets */ 'SADD' => 'Predis\Command\SetAdd', 'SREM' => 'Predis\Command\SetRemove', 'SPOP' => 'Predis\Command\SetPop', 'SMOVE' => 'Predis\Command\SetMove', 'SCARD' => 'Predis\Command\SetCardinality', 'SISMEMBER' => 'Predis\Command\SetIsMember', 'SINTER' => 'Predis\Command\SetIntersection', 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore', 'SUNION' => 'Predis\Command\SetUnion', 'SUNIONSTORE' => 'Predis\Command\SetUnionStore', 'SDIFF' => 'Predis\Command\SetDifference', 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore', 'SMEMBERS' => 'Predis\Command\SetMembers', 'SRANDMEMBER' => 'Predis\Command\SetRandomMember', /* commands operating on sorted sets */ 'ZADD' => 'Predis\Command\ZSetAdd', 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy', 'ZREM' => 'Predis\Command\ZSetRemove', 'ZRANGE' => 'Predis\Command\ZSetRange', 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange', 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore', 'ZCARD' => 'Predis\Command\ZSetCardinality', 'ZSCORE' => 'Predis\Command\ZSetScore', 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore', /* connection related commands */ 'PING' => 'Predis\Command\ConnectionPing', 'AUTH' => 'Predis\Command\ConnectionAuth', 'SELECT' => 'Predis\Command\ConnectionSelect', 'ECHO' => 'Predis\Command\ConnectionEcho', 'QUIT' => 'Predis\Command\ConnectionQuit', /* remote server control commands */ 'INFO' => 'Predis\Command\ServerInfo', 'SLAVEOF' => 'Predis\Command\ServerSlaveOf', 'MONITOR' => 'Predis\Command\ServerMonitor', 'DBSIZE' => 'Predis\Command\ServerDatabaseSize', 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase', 'FLUSHALL' => 'Predis\Command\ServerFlushAll', 'SAVE' => 'Predis\Command\ServerSave', 'BGSAVE' => 'Predis\Command\ServerBackgroundSave', 'LASTSAVE' => 'Predis\Command\ServerLastSave', 'SHUTDOWN' => 'Predis\Command\ServerShutdown', 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF', /* ---------------- Redis 2.0 ---------------- */ /* commands operating on string values */ 'SETEX' => 'Predis\Command\StringSetExpire', 'APPEND' => 'Predis\Command\StringAppend', 'SUBSTR' => 'Predis\Command\StringSubstr', /* commands operating on lists */ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking', 'BRPOP' => 'Predis\Command\ListPopLastBlocking', /* commands operating on sorted sets */ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore', 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore', 'ZCOUNT' => 'Predis\Command\ZSetCount', 'ZRANK' => 'Predis\Command\ZSetRank', 'ZREVRANK' => 'Predis\Command\ZSetReverseRank', 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank', /* commands operating on hashes */ 'HSET' => 'Predis\Command\HashSet', 'HSETNX' => 'Predis\Command\HashSetPreserve', 'HMSET' => 'Predis\Command\HashSetMultiple', 'HINCRBY' => 'Predis\Command\HashIncrementBy', 'HGET' => 'Predis\Command\HashGet', 'HMGET' => 'Predis\Command\HashGetMultiple', 'HDEL' => 'Predis\Command\HashDelete', 'HEXISTS' => 'Predis\Command\HashExists', 'HLEN' => 'Predis\Command\HashLength', 'HKEYS' => 'Predis\Command\HashKeys', 'HVALS' => 'Predis\Command\HashValues', 'HGETALL' => 'Predis\Command\HashGetAll', /* transactions */ 'MULTI' => 'Predis\Command\TransactionMulti', 'EXEC' => 'Predis\Command\TransactionExec', 'DISCARD' => 'Predis\Command\TransactionDiscard', /* publish - subscribe */ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe', 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe', 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern', 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern', 'PUBLISH' => 'Predis\Command\PubSubPublish', /* remote server control commands */ 'CONFIG' => 'Predis\Command\ServerConfig', /* ---------------- Redis 2.2 ---------------- */ /* commands operating on the key space */ 'PERSIST' => 'Predis\Command\KeyPersist', /* commands operating on string values */ 'STRLEN' => 'Predis\Command\StringStrlen', 'SETRANGE' => 'Predis\Command\StringSetRange', 'GETRANGE' => 'Predis\Command\StringGetRange', 'SETBIT' => 'Predis\Command\StringSetBit', 'GETBIT' => 'Predis\Command\StringGetBit', /* commands operating on lists */ 'RPUSHX' => 'Predis\Command\ListPushTailX', 'LPUSHX' => 'Predis\Command\ListPushHeadX', 'LINSERT' => 'Predis\Command\ListInsert', 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking', /* commands operating on sorted sets */ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore', /* transactions */ 'WATCH' => 'Predis\Command\TransactionWatch', 'UNWATCH' => 'Predis\Command\TransactionUnwatch', /* remote server control commands */ 'OBJECT' => 'Predis\Command\ServerObject', 'SLOWLOG' => 'Predis\Command\ServerSlowlog', /* ---------------- Redis 2.4 ---------------- */ /* remote server control commands */ 'CLIENT' => 'Predis\Command\ServerClient', ); } } /** * Server profile for Redis 2.0. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisVersion200 extends RedisProfile { /** * {@inheritdoc} */ public function getVersion() { return '2.0'; } /** * {@inheritdoc} */ public function getSupportedCommands() { return array( /* ---------------- Redis 1.2 ---------------- */ /* commands operating on the key space */ 'EXISTS' => 'Predis\Command\KeyExists', 'DEL' => 'Predis\Command\KeyDelete', 'TYPE' => 'Predis\Command\KeyType', 'KEYS' => 'Predis\Command\KeyKeys', 'RANDOMKEY' => 'Predis\Command\KeyRandom', 'RENAME' => 'Predis\Command\KeyRename', 'RENAMENX' => 'Predis\Command\KeyRenamePreserve', 'EXPIRE' => 'Predis\Command\KeyExpire', 'EXPIREAT' => 'Predis\Command\KeyExpireAt', 'TTL' => 'Predis\Command\KeyTimeToLive', 'MOVE' => 'Predis\Command\KeyMove', 'SORT' => 'Predis\Command\KeySort', /* commands operating on string values */ 'SET' => 'Predis\Command\StringSet', 'SETNX' => 'Predis\Command\StringSetPreserve', 'MSET' => 'Predis\Command\StringSetMultiple', 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve', 'GET' => 'Predis\Command\StringGet', 'MGET' => 'Predis\Command\StringGetMultiple', 'GETSET' => 'Predis\Command\StringGetSet', 'INCR' => 'Predis\Command\StringIncrement', 'INCRBY' => 'Predis\Command\StringIncrementBy', 'DECR' => 'Predis\Command\StringDecrement', 'DECRBY' => 'Predis\Command\StringDecrementBy', /* commands operating on lists */ 'RPUSH' => 'Predis\Command\ListPushTail', 'LPUSH' => 'Predis\Command\ListPushHead', 'LLEN' => 'Predis\Command\ListLength', 'LRANGE' => 'Predis\Command\ListRange', 'LTRIM' => 'Predis\Command\ListTrim', 'LINDEX' => 'Predis\Command\ListIndex', 'LSET' => 'Predis\Command\ListSet', 'LREM' => 'Predis\Command\ListRemove', 'LPOP' => 'Predis\Command\ListPopFirst', 'RPOP' => 'Predis\Command\ListPopLast', 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead', /* commands operating on sets */ 'SADD' => 'Predis\Command\SetAdd', 'SREM' => 'Predis\Command\SetRemove', 'SPOP' => 'Predis\Command\SetPop', 'SMOVE' => 'Predis\Command\SetMove', 'SCARD' => 'Predis\Command\SetCardinality', 'SISMEMBER' => 'Predis\Command\SetIsMember', 'SINTER' => 'Predis\Command\SetIntersection', 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore', 'SUNION' => 'Predis\Command\SetUnion', 'SUNIONSTORE' => 'Predis\Command\SetUnionStore', 'SDIFF' => 'Predis\Command\SetDifference', 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore', 'SMEMBERS' => 'Predis\Command\SetMembers', 'SRANDMEMBER' => 'Predis\Command\SetRandomMember', /* commands operating on sorted sets */ 'ZADD' => 'Predis\Command\ZSetAdd', 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy', 'ZREM' => 'Predis\Command\ZSetRemove', 'ZRANGE' => 'Predis\Command\ZSetRange', 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange', 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore', 'ZCARD' => 'Predis\Command\ZSetCardinality', 'ZSCORE' => 'Predis\Command\ZSetScore', 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore', /* connection related commands */ 'PING' => 'Predis\Command\ConnectionPing', 'AUTH' => 'Predis\Command\ConnectionAuth', 'SELECT' => 'Predis\Command\ConnectionSelect', 'ECHO' => 'Predis\Command\ConnectionEcho', 'QUIT' => 'Predis\Command\ConnectionQuit', /* remote server control commands */ 'INFO' => 'Predis\Command\ServerInfo', 'SLAVEOF' => 'Predis\Command\ServerSlaveOf', 'MONITOR' => 'Predis\Command\ServerMonitor', 'DBSIZE' => 'Predis\Command\ServerDatabaseSize', 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase', 'FLUSHALL' => 'Predis\Command\ServerFlushAll', 'SAVE' => 'Predis\Command\ServerSave', 'BGSAVE' => 'Predis\Command\ServerBackgroundSave', 'LASTSAVE' => 'Predis\Command\ServerLastSave', 'SHUTDOWN' => 'Predis\Command\ServerShutdown', 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF', /* ---------------- Redis 2.0 ---------------- */ /* commands operating on string values */ 'SETEX' => 'Predis\Command\StringSetExpire', 'APPEND' => 'Predis\Command\StringAppend', 'SUBSTR' => 'Predis\Command\StringSubstr', /* commands operating on lists */ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking', 'BRPOP' => 'Predis\Command\ListPopLastBlocking', /* commands operating on sorted sets */ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore', 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore', 'ZCOUNT' => 'Predis\Command\ZSetCount', 'ZRANK' => 'Predis\Command\ZSetRank', 'ZREVRANK' => 'Predis\Command\ZSetReverseRank', 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank', /* commands operating on hashes */ 'HSET' => 'Predis\Command\HashSet', 'HSETNX' => 'Predis\Command\HashSetPreserve', 'HMSET' => 'Predis\Command\HashSetMultiple', 'HINCRBY' => 'Predis\Command\HashIncrementBy', 'HGET' => 'Predis\Command\HashGet', 'HMGET' => 'Predis\Command\HashGetMultiple', 'HDEL' => 'Predis\Command\HashDelete', 'HEXISTS' => 'Predis\Command\HashExists', 'HLEN' => 'Predis\Command\HashLength', 'HKEYS' => 'Predis\Command\HashKeys', 'HVALS' => 'Predis\Command\HashValues', 'HGETALL' => 'Predis\Command\HashGetAll', /* transactions */ 'MULTI' => 'Predis\Command\TransactionMulti', 'EXEC' => 'Predis\Command\TransactionExec', 'DISCARD' => 'Predis\Command\TransactionDiscard', /* publish - subscribe */ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe', 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe', 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern', 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern', 'PUBLISH' => 'Predis\Command\PubSubPublish', /* remote server control commands */ 'CONFIG' => 'Predis\Command\ServerConfig', ); } } /** * Server profile for the current unstable version of Redis. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisUnstable extends RedisVersion300 { /** * {@inheritdoc} */ public function getVersion() { return '3.0'; } /** * {@inheritdoc} */ public function getSupportedCommands() { return array_merge(parent::getSupportedCommands(), array()); } } /** * Factory class for creating profile instances from strings. * * @author Daniele Alessandri <suppakilla@gmail.com> */ final class Factory { private static $profiles = array( '2.0' => 'Predis\Profile\RedisVersion200', '2.2' => 'Predis\Profile\RedisVersion220', '2.4' => 'Predis\Profile\RedisVersion240', '2.6' => 'Predis\Profile\RedisVersion260', '2.8' => 'Predis\Profile\RedisVersion280', '3.0' => 'Predis\Profile\RedisVersion300', 'default' => 'Predis\Profile\RedisVersion300', 'dev' => 'Predis\Profile\RedisUnstable', ); /** * */ private function __construct() { // NOOP } /** * Returns the default server profile. * * @return ProfileInterface */ public static function getDefault() { return self::get('default'); } /** * Returns the development server profile. * * @return ProfileInterface */ public static function getDevelopment() { return self::get('dev'); } /** * Registers a new server profile. * * @param string $alias Profile version or alias. * @param string $class FQN of a class implementing Predis\Profile\ProfileInterface. * * @throws \InvalidArgumentException */ public static function define($alias, $class) { $reflection = new ReflectionClass($class); if (!$reflection->isSubclassOf('Predis\Profile\ProfileInterface')) { throw new InvalidArgumentException("The class '$class' is not a valid profile class."); } self::$profiles[$alias] = $class; } /** * Returns the specified server profile. * * @param string $version Profile version or alias. * * @return ProfileInterface * * @throws ClientException */ public static function get($version) { if (!isset(self::$profiles[$version])) { throw new ClientException("Unknown server profile: '$version'."); } $profile = self::$profiles[$version]; return new $profile(); } } /** * Server profile for Redis 2.2. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisVersion220 extends RedisProfile { /** * {@inheritdoc} */ public function getVersion() { return '2.2'; } /** * {@inheritdoc} */ public function getSupportedCommands() { return array( /* ---------------- Redis 1.2 ---------------- */ /* commands operating on the key space */ 'EXISTS' => 'Predis\Command\KeyExists', 'DEL' => 'Predis\Command\KeyDelete', 'TYPE' => 'Predis\Command\KeyType', 'KEYS' => 'Predis\Command\KeyKeys', 'RANDOMKEY' => 'Predis\Command\KeyRandom', 'RENAME' => 'Predis\Command\KeyRename', 'RENAMENX' => 'Predis\Command\KeyRenamePreserve', 'EXPIRE' => 'Predis\Command\KeyExpire', 'EXPIREAT' => 'Predis\Command\KeyExpireAt', 'TTL' => 'Predis\Command\KeyTimeToLive', 'MOVE' => 'Predis\Command\KeyMove', 'SORT' => 'Predis\Command\KeySort', /* commands operating on string values */ 'SET' => 'Predis\Command\StringSet', 'SETNX' => 'Predis\Command\StringSetPreserve', 'MSET' => 'Predis\Command\StringSetMultiple', 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve', 'GET' => 'Predis\Command\StringGet', 'MGET' => 'Predis\Command\StringGetMultiple', 'GETSET' => 'Predis\Command\StringGetSet', 'INCR' => 'Predis\Command\StringIncrement', 'INCRBY' => 'Predis\Command\StringIncrementBy', 'DECR' => 'Predis\Command\StringDecrement', 'DECRBY' => 'Predis\Command\StringDecrementBy', /* commands operating on lists */ 'RPUSH' => 'Predis\Command\ListPushTail', 'LPUSH' => 'Predis\Command\ListPushHead', 'LLEN' => 'Predis\Command\ListLength', 'LRANGE' => 'Predis\Command\ListRange', 'LTRIM' => 'Predis\Command\ListTrim', 'LINDEX' => 'Predis\Command\ListIndex', 'LSET' => 'Predis\Command\ListSet', 'LREM' => 'Predis\Command\ListRemove', 'LPOP' => 'Predis\Command\ListPopFirst', 'RPOP' => 'Predis\Command\ListPopLast', 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead', /* commands operating on sets */ 'SADD' => 'Predis\Command\SetAdd', 'SREM' => 'Predis\Command\SetRemove', 'SPOP' => 'Predis\Command\SetPop', 'SMOVE' => 'Predis\Command\SetMove', 'SCARD' => 'Predis\Command\SetCardinality', 'SISMEMBER' => 'Predis\Command\SetIsMember', 'SINTER' => 'Predis\Command\SetIntersection', 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore', 'SUNION' => 'Predis\Command\SetUnion', 'SUNIONSTORE' => 'Predis\Command\SetUnionStore', 'SDIFF' => 'Predis\Command\SetDifference', 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore', 'SMEMBERS' => 'Predis\Command\SetMembers', 'SRANDMEMBER' => 'Predis\Command\SetRandomMember', /* commands operating on sorted sets */ 'ZADD' => 'Predis\Command\ZSetAdd', 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy', 'ZREM' => 'Predis\Command\ZSetRemove', 'ZRANGE' => 'Predis\Command\ZSetRange', 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange', 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore', 'ZCARD' => 'Predis\Command\ZSetCardinality', 'ZSCORE' => 'Predis\Command\ZSetScore', 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore', /* connection related commands */ 'PING' => 'Predis\Command\ConnectionPing', 'AUTH' => 'Predis\Command\ConnectionAuth', 'SELECT' => 'Predis\Command\ConnectionSelect', 'ECHO' => 'Predis\Command\ConnectionEcho', 'QUIT' => 'Predis\Command\ConnectionQuit', /* remote server control commands */ 'INFO' => 'Predis\Command\ServerInfo', 'SLAVEOF' => 'Predis\Command\ServerSlaveOf', 'MONITOR' => 'Predis\Command\ServerMonitor', 'DBSIZE' => 'Predis\Command\ServerDatabaseSize', 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase', 'FLUSHALL' => 'Predis\Command\ServerFlushAll', 'SAVE' => 'Predis\Command\ServerSave', 'BGSAVE' => 'Predis\Command\ServerBackgroundSave', 'LASTSAVE' => 'Predis\Command\ServerLastSave', 'SHUTDOWN' => 'Predis\Command\ServerShutdown', 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF', /* ---------------- Redis 2.0 ---------------- */ /* commands operating on string values */ 'SETEX' => 'Predis\Command\StringSetExpire', 'APPEND' => 'Predis\Command\StringAppend', 'SUBSTR' => 'Predis\Command\StringSubstr', /* commands operating on lists */ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking', 'BRPOP' => 'Predis\Command\ListPopLastBlocking', /* commands operating on sorted sets */ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore', 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore', 'ZCOUNT' => 'Predis\Command\ZSetCount', 'ZRANK' => 'Predis\Command\ZSetRank', 'ZREVRANK' => 'Predis\Command\ZSetReverseRank', 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank', /* commands operating on hashes */ 'HSET' => 'Predis\Command\HashSet', 'HSETNX' => 'Predis\Command\HashSetPreserve', 'HMSET' => 'Predis\Command\HashSetMultiple', 'HINCRBY' => 'Predis\Command\HashIncrementBy', 'HGET' => 'Predis\Command\HashGet', 'HMGET' => 'Predis\Command\HashGetMultiple', 'HDEL' => 'Predis\Command\HashDelete', 'HEXISTS' => 'Predis\Command\HashExists', 'HLEN' => 'Predis\Command\HashLength', 'HKEYS' => 'Predis\Command\HashKeys', 'HVALS' => 'Predis\Command\HashValues', 'HGETALL' => 'Predis\Command\HashGetAll', /* transactions */ 'MULTI' => 'Predis\Command\TransactionMulti', 'EXEC' => 'Predis\Command\TransactionExec', 'DISCARD' => 'Predis\Command\TransactionDiscard', /* publish - subscribe */ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe', 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe', 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern', 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern', 'PUBLISH' => 'Predis\Command\PubSubPublish', /* remote server control commands */ 'CONFIG' => 'Predis\Command\ServerConfig', /* ---------------- Redis 2.2 ---------------- */ /* commands operating on the key space */ 'PERSIST' => 'Predis\Command\KeyPersist', /* commands operating on string values */ 'STRLEN' => 'Predis\Command\StringStrlen', 'SETRANGE' => 'Predis\Command\StringSetRange', 'GETRANGE' => 'Predis\Command\StringGetRange', 'SETBIT' => 'Predis\Command\StringSetBit', 'GETBIT' => 'Predis\Command\StringGetBit', /* commands operating on lists */ 'RPUSHX' => 'Predis\Command\ListPushTailX', 'LPUSHX' => 'Predis\Command\ListPushHeadX', 'LINSERT' => 'Predis\Command\ListInsert', 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking', /* commands operating on sorted sets */ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore', /* transactions */ 'WATCH' => 'Predis\Command\TransactionWatch', 'UNWATCH' => 'Predis\Command\TransactionUnwatch', /* remote server control commands */ 'OBJECT' => 'Predis\Command\ServerObject', 'SLOWLOG' => 'Predis\Command\ServerSlowlog', ); } } /* --------------------------------------------------------------------------- */ namespace Predis; use InvalidArgumentException; use UnexpectedValueException; use Predis\Command\CommandInterface; use Predis\Command\RawCommand; use Predis\Command\ScriptCommand; use Predis\Configuration\Options; use Predis\Configuration\OptionsInterface; use Predis\Connection\ConnectionInterface; use Predis\Connection\AggregateConnectionInterface; use Predis\Connection\ParametersInterface; use Predis\Monitor\Consumer as MonitorConsumer; use Predis\Pipeline\Pipeline; use Predis\PubSub\Consumer as PubSubConsumer; use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\ResponseInterface; use Predis\Response\ServerException; use Predis\Transaction\MultiExec as MultiExecTransaction; use Predis\Profile\ProfileInterface; use Exception; use Predis\Connection\NodeConnectionInterface; /** * Base exception class for Predis-related errors. * * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class PredisException extends Exception { } /** * Interface defining a client-side context such as a pipeline or transaction. * * @method $this del(array $keys) * @method $this dump($key) * @method $this exists($key) * @method $this expire($key, $seconds) * @method $this expireat($key, $timestamp) * @method $this keys($pattern) * @method $this move($key, $db) * @method $this object($subcommand, $key) * @method $this persist($key) * @method $this pexpire($key, $milliseconds) * @method $this pexpireat($key, $timestamp) * @method $this pttl($key) * @method $this randomkey() * @method $this rename($key, $target) * @method $this renamenx($key, $target) * @method $this scan($cursor, array $options = null) * @method $this sort($key, array $options = null) * @method $this ttl($key) * @method $this type($key) * @method $this append($key, $value) * @method $this bitcount($key, $start = null, $end = null) * @method $this bitop($operation, $destkey, $key) * @method $this decr($key) * @method $this decrby($key, $decrement) * @method $this get($key) * @method $this getbit($key, $offset) * @method $this getrange($key, $start, $end) * @method $this getset($key, $value) * @method $this incr($key) * @method $this incrby($key, $increment) * @method $this incrbyfloat($key, $increment) * @method $this mget(array $keys) * @method $this mset(array $dictionary) * @method $this msetnx(array $dictionary) * @method $this psetex($key, $milliseconds, $value) * @method $this set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null) * @method $this setbit($key, $offset, $value) * @method $this setex($key, $seconds, $value) * @method $this setnx($key, $value) * @method $this setrange($key, $offset, $value) * @method $this strlen($key) * @method $this hdel($key, array $fields) * @method $this hexists($key, $field) * @method $this hget($key, $field) * @method $this hgetall($key) * @method $this hincrby($key, $field, $increment) * @method $this hincrbyfloat($key, $field, $increment) * @method $this hkeys($key) * @method $this hlen($key) * @method $this hmget($key, array $fields) * @method $this hmset($key, array $dictionary) * @method $this hscan($key, $cursor, array $options = null) * @method $this hset($key, $field, $value) * @method $this hsetnx($key, $field, $value) * @method $this hvals($key) * @method $this blpop(array $keys, $timeout) * @method $this brpop(array $keys, $timeout) * @method $this brpoplpush($source, $destination, $timeout) * @method $this lindex($key, $index) * @method $this linsert($key, $whence, $pivot, $value) * @method $this llen($key) * @method $this lpop($key) * @method $this lpush($key, array $values) * @method $this lpushx($key, $value) * @method $this lrange($key, $start, $stop) * @method $this lrem($key, $count, $value) * @method $this lset($key, $index, $value) * @method $this ltrim($key, $start, $stop) * @method $this rpop($key) * @method $this rpoplpush($source, $destination) * @method $this rpush($key, array $values) * @method $this rpushx($key, $value) * @method $this sadd($key, array $members) * @method $this scard($key) * @method $this sdiff(array $keys) * @method $this sdiffstore($destination, array $keys) * @method $this sinter(array $keys) * @method $this sinterstore($destination, array $keys) * @method $this sismember($key, $member) * @method $this smembers($key) * @method $this smove($source, $destination, $member) * @method $this spop($key) * @method $this srandmember($key, $count = null) * @method $this srem($key, $member) * @method $this sscan($key, $cursor, array $options = null) * @method $this sunion(array $keys) * @method $this sunionstore($destination, array $keys) * @method $this zadd($key, array $membersAndScoresDictionary) * @method $this zcard($key) * @method $this zcount($key, $min, $max) * @method $this zincrby($key, $increment, $member) * @method $this zinterstore($destination, array $keys, array $options = null) * @method $this zrange($key, $start, $stop, array $options = null) * @method $this zrangebyscore($key, $min, $max, array $options = null) * @method $this zrank($key, $member) * @method $this zrem($key, $member) * @method $this zremrangebyrank($key, $start, $stop) * @method $this zremrangebyscore($key, $min, $max) * @method $this zrevrange($key, $start, $stop, array $options = null) * @method $this zrevrangebyscore($key, $min, $max, array $options = null) * @method $this zrevrank($key, $member) * @method $this zunionstore($destination, array $keys, array $options = null) * @method $this zscore($key, $member) * @method $this zscan($key, $cursor, array $options = null) * @method $this zrangebylex($key, $start, $stop, array $options = null) * @method $this zremrangebylex($key, $min, $max) * @method $this zlexcount($key, $min, $max) * @method $this pfadd($key, array $elements) * @method $this pfmerge($destinationKey, array $sourceKeys) * @method $this pfcount(array $keys) * @method $this pubsub($subcommand, $argument) * @method $this publish($channel, $message) * @method $this discard() * @method $this exec() * @method $this multi() * @method $this unwatch() * @method $this watch($key) * @method $this eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null) * @method $this evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null) * @method $this script($subcommand, $argument = null) * @method $this auth($password) * @method $this echo($message) * @method $this ping($message = null) * @method $this select($database) * @method $this bgrewriteaof() * @method $this bgsave() * @method $this client($subcommand, $argument = null) * @method $this config($subcommand, $argument = null) * @method $this dbsize() * @method $this flushall() * @method $this flushdb() * @method $this info($section = null) * @method $this lastsave() * @method $this save() * @method $this slaveof($host, $port) * @method $this slowlog($subcommand, $argument = null) * @method $this time() * @method $this command() * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ClientContextInterface { /** * Sends the specified command instance to Redis. * * @param CommandInterface $command Command instance. * * @return mixed */ public function executeCommand(CommandInterface $command); /** * Sends the specified command with its arguments to Redis. * * @param string $method Command ID. * @param array $arguments Arguments for the command. * * @return mixed */ public function __call($method, $arguments); /** * Starts the execution of the context. * * @param mixed $callable Optional callback for execution. * * @return array */ public function execute($callable = null); } /** * Base exception class for network-related errors. * * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class CommunicationException extends PredisException { private $connection; /** * @param NodeConnectionInterface $connection Connection that generated the exception. * @param string $message Error message. * @param int $code Error code. * @param Exception $innerException Inner exception for wrapping the original error. */ public function __construct( NodeConnectionInterface $connection, $message = null, $code = null, Exception $innerException = null ) { parent::__construct($message, $code, $innerException); $this->connection = $connection; } /** * Gets the connection that generated the exception. * * @return NodeConnectionInterface */ public function getConnection() { return $this->connection; } /** * Indicates if the receiver should reset the underlying connection. * * @return bool */ public function shouldResetConnection() { return true; } /** * Helper method to handle exceptions generated by a connection object. * * @param CommunicationException $exception Exception. * * @throws CommunicationException */ public static function handle(CommunicationException $exception) { if ($exception->shouldResetConnection()) { $connection = $exception->getConnection(); if ($connection->isConnected()) { $connection->disconnect(); } } throw $exception; } } /** * Interface defining a client able to execute commands against Redis. * * All the commands exposed by the client generally have the same signature as * described by the Redis documentation, but some of them offer an additional * and more friendly interface to ease programming which is described in the * following list of methods: * * @method int del(array $keys) * @method string dump($key) * @method int exists($key) * @method int expire($key, $seconds) * @method int expireat($key, $timestamp) * @method array keys($pattern) * @method int move($key, $db) * @method mixed object($subcommand, $key) * @method int persist($key) * @method int pexpire($key, $milliseconds) * @method int pexpireat($key, $timestamp) * @method int pttl($key) * @method string randomkey() * @method mixed rename($key, $target) * @method int renamenx($key, $target) * @method array scan($cursor, array $options = null) * @method array sort($key, array $options = null) * @method int ttl($key) * @method mixed type($key) * @method int append($key, $value) * @method int bitcount($key, $start = null, $end = null) * @method int bitop($operation, $destkey, $key) * @method int decr($key) * @method int decrby($key, $decrement) * @method string get($key) * @method int getbit($key, $offset) * @method string getrange($key, $start, $end) * @method string getset($key, $value) * @method int incr($key) * @method int incrby($key, $increment) * @method string incrbyfloat($key, $increment) * @method array mget(array $keys) * @method mixed mset(array $dictionary) * @method int msetnx(array $dictionary) * @method mixed psetex($key, $milliseconds, $value) * @method mixed set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null) * @method int setbit($key, $offset, $value) * @method int setex($key, $seconds, $value) * @method int setnx($key, $value) * @method int setrange($key, $offset, $value) * @method int strlen($key) * @method int hdel($key, array $fields) * @method int hexists($key, $field) * @method string hget($key, $field) * @method array hgetall($key) * @method int hincrby($key, $field, $increment) * @method string hincrbyfloat($key, $field, $increment) * @method array hkeys($key) * @method int hlen($key) * @method array hmget($key, array $fields) * @method mixed hmset($key, array $dictionary) * @method array hscan($key, $cursor, array $options = null) * @method int hset($key, $field, $value) * @method int hsetnx($key, $field, $value) * @method array hvals($key) * @method array blpop(array $keys, $timeout) * @method array brpop(array $keys, $timeout) * @method array brpoplpush($source, $destination, $timeout) * @method string lindex($key, $index) * @method int linsert($key, $whence, $pivot, $value) * @method int llen($key) * @method string lpop($key) * @method int lpush($key, array $values) * @method int lpushx($key, $value) * @method array lrange($key, $start, $stop) * @method int lrem($key, $count, $value) * @method mixed lset($key, $index, $value) * @method mixed ltrim($key, $start, $stop) * @method string rpop($key) * @method string rpoplpush($source, $destination) * @method int rpush($key, array $values) * @method int rpushx($key, $value) * @method int sadd($key, array $members) * @method int scard($key) * @method array sdiff(array $keys) * @method int sdiffstore($destination, array $keys) * @method array sinter(array $keys) * @method int sinterstore($destination, array $keys) * @method int sismember($key, $member) * @method array smembers($key) * @method int smove($source, $destination, $member) * @method string spop($key) * @method string srandmember($key, $count = null) * @method int srem($key, $member) * @method array sscan($key, $cursor, array $options = null) * @method array sunion(array $keys) * @method int sunionstore($destination, array $keys) * @method int zadd($key, array $membersAndScoresDictionary) * @method int zcard($key) * @method string zcount($key, $min, $max) * @method string zincrby($key, $increment, $member) * @method int zinterstore($destination, array $keys, array $options = null) * @method array zrange($key, $start, $stop, array $options = null) * @method array zrangebyscore($key, $min, $max, array $options = null) * @method int zrank($key, $member) * @method int zrem($key, $member) * @method int zremrangebyrank($key, $start, $stop) * @method int zremrangebyscore($key, $min, $max) * @method array zrevrange($key, $start, $stop, array $options = null) * @method array zrevrangebyscore($key, $min, $max, array $options = null) * @method int zrevrank($key, $member) * @method int zunionstore($destination, array $keys, array $options = null) * @method string zscore($key, $member) * @method array zscan($key, $cursor, array $options = null) * @method array zrangebylex($key, $start, $stop, array $options = null) * @method int zremrangebylex($key, $min, $max) * @method int zlexcount($key, $min, $max) * @method int pfadd($key, array $elements) * @method mixed pfmerge($destinationKey, array $sourceKeys) * @method int pfcount(array $keys) * @method mixed pubsub($subcommand, $argument) * @method int publish($channel, $message) * @method mixed discard() * @method array exec() * @method mixed multi() * @method mixed unwatch() * @method mixed watch($key) * @method mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null) * @method mixed evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null) * @method mixed script($subcommand, $argument = null) * @method mixed auth($password) * @method string echo($message) * @method mixed ping($message = null) * @method mixed select($database) * @method mixed bgrewriteaof() * @method mixed bgsave() * @method mixed client($subcommand, $argument = null) * @method mixed config($subcommand, $argument = null) * @method int dbsize() * @method mixed flushall() * @method mixed flushdb() * @method array info($section = null) * @method int lastsave() * @method mixed save() * @method mixed slaveof($host, $port) * @method mixed slowlog($subcommand, $argument = null) * @method array time() * @method array command() * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ClientInterface { /** * Returns the server profile used by the client. * * @return ProfileInterface */ public function getProfile(); /** * Returns the client options specified upon initialization. * * @return OptionsInterface */ public function getOptions(); /** * Opens the underlying connection to the server. */ public function connect(); /** * Closes the underlying connection from the server. */ public function disconnect(); /** * Returns the underlying connection instance. * * @return ConnectionInterface */ public function getConnection(); /** * Creates a new instance of the specified Redis command. * * @param string $method Command ID. * @param array $arguments Arguments for the command. * * @return CommandInterface */ public function createCommand($method, $arguments = array()); /** * Executes the specified Redis command. * * @param CommandInterface $command Command instance. * * @return mixed */ public function executeCommand(CommandInterface $command); /** * Creates a Redis command with the specified arguments and sends a request * to the server. * * @param string $method Command ID. * @param array $arguments Arguments for the command. * * @return mixed */ public function __call($method, $arguments); } /** * Exception class thrown when trying to use features not supported by certain * classes or abstractions of Predis. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class NotSupportedException extends PredisException { } /** * Exception class that identifies client-side errors. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ClientException extends PredisException { } /** * Client class used for connecting and executing commands on Redis. * * This is the main high-level abstraction of Predis upon which various other * abstractions are built. Internally it aggregates various other classes each * one with its own responsibility and scope. * * {@inheritdoc} * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Client implements ClientInterface { const VERSION = '1.0.1'; protected $connection; protected $options; private $profile; /** * @param mixed $parameters Connection parameters for one or more servers. * @param mixed $options Options to configure some behaviours of the client. */ public function __construct($parameters = null, $options = null) { $this->options = $this->createOptions($options ?: array()); $this->connection = $this->createConnection($parameters ?: array()); $this->profile = $this->options->profile; } /** * Creates a new instance of Predis\Configuration\Options from different * types of arguments or simply returns the passed argument if it is an * instance of Predis\Configuration\OptionsInterface. * * @param mixed $options Client options. * * @return OptionsInterface * * @throws \InvalidArgumentException */ protected function createOptions($options) { if (is_array($options)) { return new Options($options); } if ($options instanceof OptionsInterface) { return $options; } throw new InvalidArgumentException("Invalid type for client options."); } /** * Creates single or aggregate connections from different types of arguments * (string, array) or returns the passed argument if it is an instance of a * class implementing Predis\Connection\ConnectionInterface. * * Accepted types for connection parameters are: * * - Instance of Predis\Connection\ConnectionInterface. * - Instance of Predis\Connection\ParametersInterface. * - Array * - String * - Callable * * @param mixed $parameters Connection parameters or connection instance. * * @return ConnectionInterface * * @throws \InvalidArgumentException */ protected function createConnection($parameters) { if ($parameters instanceof ConnectionInterface) { return $parameters; } if ($parameters instanceof ParametersInterface || is_string($parameters)) { return $this->options->connections->create($parameters); } if (is_array($parameters)) { if (!isset($parameters[0])) { return $this->options->connections->create($parameters); } $options = $this->options; if ($options->defined('aggregate')) { $initializer = $this->getConnectionInitializerWrapper($options->aggregate); $connection = $initializer($parameters, $options); } else { if ($options->defined('replication') && $replication = $options->replication) { $connection = $replication; } else { $connection = $options->cluster; } $options->connections->aggregate($connection, $parameters); } return $connection; } if (is_callable($parameters)) { $initializer = $this->getConnectionInitializerWrapper($parameters); $connection = $initializer($this->options); return $connection; } throw new InvalidArgumentException('Invalid type for connection parameters.'); } /** * Wraps a callable to make sure that its returned value represents a valid * connection type. * * @param mixed $callable * * @return \Closure */ protected function getConnectionInitializerWrapper($callable) { return function () use ($callable) { $connection = call_user_func_array($callable, func_get_args()); if (!$connection instanceof ConnectionInterface) { throw new UnexpectedValueException( 'The callable connection initializer returned an invalid type.' ); } return $connection; }; } /** * {@inheritdoc} */ public function getProfile() { return $this->profile; } /** * {@inheritdoc} */ public function getOptions() { return $this->options; } /** * Creates a new client instance for the specified connection ID or alias, * only when working with an aggregate connection (cluster, replication). * The new client instances uses the same options of the original one. * * @param string $connectionID Identifier of a connection. * * @return Client * * @throws \InvalidArgumentException */ public function getClientFor($connectionID) { if (!$connection = $this->getConnectionById($connectionID)) { throw new InvalidArgumentException("Invalid connection ID: $connectionID."); } return new static($connection, $this->options); } /** * Opens the underlying connection and connects to the server. */ public function connect() { $this->connection->connect(); } /** * Closes the underlying connection and disconnects from the server. */ public function disconnect() { $this->connection->disconnect(); } /** * Closes the underlying connection and disconnects from the server. * * This is the same as `Client::disconnect()` as it does not actually send * the `QUIT` command to Redis, but simply closes the connection. */ public function quit() { $this->disconnect(); } /** * Returns the current state of the underlying connection. * * @return bool */ public function isConnected() { return $this->connection->isConnected(); } /** * {@inheritdoc} */ public function getConnection() { return $this->connection; } /** * Retrieves the specified connection from the aggregate connection when the * client is in cluster or replication mode. * * @param string $connectionID Index or alias of the single connection. * * @return Connection\NodeConnectionInterface * * @throws NotSupportedException */ public function getConnectionById($connectionID) { if (!$this->connection instanceof AggregateConnectionInterface) { throw new NotSupportedException( 'Retrieving connections by ID is supported only by aggregate connections.' ); } return $this->connection->getConnectionById($connectionID); } /** * Executes a command without filtering its arguments, parsing the response, * applying any prefix to keys or throwing exceptions on Redis errors even * regardless of client options. * * It is possibile to indentify Redis error responses from normal responses * using the second optional argument which is populated by reference. * * @param array $arguments Command arguments as defined by the command signature. * @param bool $error Set to TRUE when Redis returned an error response. * * @return mixed */ public function executeRaw(array $arguments, &$error = null) { $error = false; $response = $this->connection->executeCommand( new RawCommand($arguments) ); if ($response instanceof ResponseInterface) { if ($response instanceof ErrorResponseInterface) { $error = true; } return (string) $response; } return $response; } /** * {@inheritdoc} */ public function __call($commandID, $arguments) { return $this->executeCommand( $this->createCommand($commandID, $arguments) ); } /** * {@inheritdoc} */ public function createCommand($commandID, $arguments = array()) { return $this->profile->createCommand($commandID, $arguments); } /** * {@inheritdoc} */ public function executeCommand(CommandInterface $command) { $response = $this->connection->executeCommand($command); if ($response instanceof ResponseInterface) { if ($response instanceof ErrorResponseInterface) { $response = $this->onErrorResponse($command, $response); } return $response; } return $command->parseResponse($response); } /** * Handles -ERR responses returned by Redis. * * @param CommandInterface $command Redis command that generated the error. * @param ErrorResponseInterface $response Instance of the error response. * * @return mixed * * @throws ServerException */ protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response) { if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') { $eval = $this->createCommand('EVAL'); $eval->setRawArguments($command->getEvalArguments()); $response = $this->executeCommand($eval); if (!$response instanceof ResponseInterface) { $response = $command->parseResponse($response); } return $response; } if ($this->options->exceptions) { throw new ServerException($response->getMessage()); } return $response; } /** * Executes the specified initializer method on `$this` by adjusting the * actual invokation depending on the arity (0, 1 or 2 arguments). This is * simply an utility method to create Redis contexts instances since they * follow a common initialization path. * * @param string $initializer Method name. * @param array $argv Arguments for the method. * * @return mixed */ private function sharedContextFactory($initializer, $argv = null) { switch (count($argv)) { case 0: return $this->$initializer(); case 1: return is_array($argv[0]) ? $this->$initializer($argv[0]) : $this->$initializer(null, $argv[0]); case 2: list($arg0, $arg1) = $argv; return $this->$initializer($arg0, $arg1); default: return $this->$initializer($this, $argv); } } /** * Creates a new pipeline context and returns it, or returns the results of * a pipeline executed inside the optionally provided callable object. * * @param mixed ... Array of options, a callable for execution, or both. * * @return Pipeline|array */ public function pipeline(/* arguments */) { return $this->sharedContextFactory('createPipeline', func_get_args()); } /** * Actual pipeline context initializer method. * * @param array $options Options for the context. * @param mixed $callable Optional callable used to execute the context. * * @return Pipeline|array */ protected function createPipeline(array $options = null, $callable = null) { if (isset($options['atomic']) && $options['atomic']) { $class = 'Predis\Pipeline\Atomic'; } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) { $class = 'Predis\Pipeline\FireAndForget'; } else { $class = 'Predis\Pipeline\Pipeline'; } /* * @var ClientContextInterface */ $pipeline = new $class($this); if (isset($callable)) { return $pipeline->execute($callable); } return $pipeline; } /** * Creates a new transaction context and returns it, or returns the results * of a transaction executed inside the optionally provided callable object. * * @param mixed ... Array of options, a callable for execution, or both. * * @return MultiExecTransaction|array */ public function transaction(/* arguments */) { return $this->sharedContextFactory('createTransaction', func_get_args()); } /** * Actual transaction context initializer method. * * @param array $options Options for the context. * @param mixed $callable Optional callable used to execute the context. * * @return MultiExecTransaction|array */ protected function createTransaction(array $options = null, $callable = null) { $transaction = new MultiExecTransaction($this, $options); if (isset($callable)) { return $transaction->execute($callable); } return $transaction; } /** * Creates a new publis/subscribe context and returns it, or starts its loop * inside the optionally provided callable object. * * @param mixed ... Array of options, a callable for execution, or both. * * @return PubSubConsumer|null */ public function pubSubLoop(/* arguments */) { return $this->sharedContextFactory('createPubSub', func_get_args()); } /** * Actual publish/subscribe context initializer method. * * @param array $options Options for the context. * @param mixed $callable Optional callable used to execute the context. * * @return PubSubConsumer|null */ protected function createPubSub(array $options = null, $callable = null) { $pubsub = new PubSubConsumer($this, $options); if (!isset($callable)) { return $pubsub; } foreach ($pubsub as $message) { if (call_user_func($callable, $pubsub, $message) === false) { $pubsub->stop(); } } } /** * Creates a new monitor consumer and returns it. * * @return MonitorConsumer */ public function monitor() { return new MonitorConsumer($this); } } /** * Implements a lightweight PSR-0 compliant autoloader for Predis. * * @author Eric Naeseth <eric@thumbtack.com> * @author Daniele Alessandri <suppakilla@gmail.com> */ class Autoloader { private $directory; private $prefix; private $prefixLength; /** * @param string $baseDirectory Base directory where the source files are located. */ public function __construct($baseDirectory = __DIR__) { $this->directory = $baseDirectory; $this->prefix = __NAMESPACE__ . '\\'; $this->prefixLength = strlen($this->prefix); } /** * Registers the autoloader class with the PHP SPL autoloader. * * @param bool $prepend Prepend the autoloader on the stack instead of appending it. */ public static function register($prepend = false) { spl_autoload_register(array(new self, 'autoload'), true, $prepend); } /** * Loads a class from a file using its fully qualified name. * * @param string $className Fully qualified name of a class. */ public function autoload($className) { if (0 === strpos($className, $this->prefix)) { $parts = explode('\\', substr($className, $this->prefixLength)); $filepath = $this->directory.DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts).'.php'; if (is_file($filepath)) { require($filepath); } } } } /* --------------------------------------------------------------------------- */ namespace Predis\Configuration; use InvalidArgumentException; use Predis\Connection\Aggregate\ClusterInterface; use Predis\Connection\Aggregate\PredisCluster; use Predis\Connection\Aggregate\RedisCluster; use Predis\Connection\Factory; use Predis\Connection\FactoryInterface; use Predis\Command\Processor\KeyPrefixProcessor; use Predis\Command\Processor\ProcessorInterface; use Predis\Profile\Factory as Predis_Factory; use Predis\Profile\ProfileInterface; use Predis\Profile\RedisProfile; use Predis\Connection\Aggregate\MasterSlaveReplication; use Predis\Connection\Aggregate\ReplicationInterface; /** * Defines an handler used by Predis\Configuration\Options to filter, validate * or return default values for a given option. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface OptionInterface { /** * Filters and validates the passed value. * * @param OptionsInterface $options Options container. * @param mixed $value Input value. * * @return mixed */ public function filter(OptionsInterface $options, $value); /** * Returns the default value for the option. * * @param OptionsInterface $options Options container. * * @return mixed */ public function getDefault(OptionsInterface $options); } /** * Interface defining a container for client options. * * @property-read mixed aggregate Custom connection aggregator. * @property-read mixed cluster Aggregate connection for clustering. * @property-read mixed connections Connection factory. * @property-read mixed exceptions Toggles exceptions in client for -ERR responses. * @property-read mixed prefix Key prefixing strategy using the given prefix. * @property-read mixed profile Server profile. * @property-read mixed replication Aggregate connection for replication. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface OptionsInterface { /** * Returns the default value for the given option. * * @param string $option Name of the option. * * @return mixed|null */ public function getDefault($option); /** * Checks if the given option has been set by the user upon initialization. * * @param string $option Name of the option. * * @return bool */ public function defined($option); /** * Checks if the given option has been set and does not evaluate to NULL. * * @param string $option Name of the option. * * @return bool */ public function __isset($option); /** * Returns the value of the given option. * * @param string $option Name of the option. * * @return mixed|null */ public function __get($option); } /** * Configures a command processor that apply the specified prefix string to a * series of Redis commands considered prefixable. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class PrefixOption implements OptionInterface { /** * {@inheritdoc} */ public function filter(OptionsInterface $options, $value) { if ($value instanceof ProcessorInterface) { return $value; } return new KeyPrefixProcessor($value); } /** * {@inheritdoc} */ public function getDefault(OptionsInterface $options) { // NOOP } } /** * Configures the server profile to be used by the client to create command * instances depending on the specified version of the Redis server. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ProfileOption implements OptionInterface { /** * Sets the commands processors that need to be applied to the profile. * * @param OptionsInterface $options Client options. * @param ProfileInterface $profile Server profile. */ protected function setProcessors(OptionsInterface $options, ProfileInterface $profile) { if (isset($options->prefix) && $profile instanceof RedisProfile) { // NOTE: directly using __get('prefix') is actually a workaround for // HHVM 2.3.0. It's correct and respects the options interface, it's // just ugly. We will remove this hack when HHVM will fix re-entrant // calls to __get() once and for all. $profile->setProcessor($options->__get('prefix')); } } /** * {@inheritdoc} */ public function filter(OptionsInterface $options, $value) { if (is_string($value)) { $value = Predis_Factory::get($value); $this->setProcessors($options, $value); } elseif (!$value instanceof ProfileInterface) { throw new InvalidArgumentException('Invalid value for the profile option.'); } return $value; } /** * {@inheritdoc} */ public function getDefault(OptionsInterface $options) { $profile = Predis_Factory::getDefault(); $this->setProcessors($options, $profile); return $profile; } } /** * Configures an aggregate connection used for master/slave replication among * multiple Redis nodes. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ReplicationOption implements OptionInterface { /** * {@inheritdoc} * * @todo There's more code than needed due to a bug in filter_var() as * discussed here https://bugs.php.net/bug.php?id=49510 and different * behaviours when encountering NULL values on PHP 5.3. */ public function filter(OptionsInterface $options, $value) { if ($value instanceof ReplicationInterface) { return $value; } if (is_bool($value) || $value === null) { return $value ? $this->getDefault($options) : null; } if ( !is_object($value) && null !== $asbool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ) { return $asbool ? $this->getDefault($options) : null; } throw new InvalidArgumentException( "An instance of type 'Predis\Connection\Aggregate\ReplicationInterface' was expected." ); } /** * {@inheritdoc} */ public function getDefault(OptionsInterface $options) { return new MasterSlaveReplication(); } } /** * Manages Predis options with filtering, conversion and lazy initialization of * values using a mini-DI container approach. * * {@inheritdoc} * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Options implements OptionsInterface { protected $input; protected $options; protected $handlers; /** * @param array $options Array of options with their values */ public function __construct(array $options = array()) { $this->input = $options; $this->options = array(); $this->handlers = $this->getHandlers(); } /** * Ensures that the default options are initialized. * * @return array */ protected function getHandlers() { return array( 'cluster' => 'Predis\Configuration\ClusterOption', 'connections' => 'Predis\Configuration\ConnectionFactoryOption', 'exceptions' => 'Predis\Configuration\ExceptionsOption', 'prefix' => 'Predis\Configuration\PrefixOption', 'profile' => 'Predis\Configuration\ProfileOption', 'replication' => 'Predis\Configuration\ReplicationOption', ); } /** * {@inheritdoc} */ public function getDefault($option) { if (isset($this->handlers[$option])) { $handler = $this->handlers[$option]; $handler = new $handler(); return $handler->getDefault($this); } } /** * {@inheritdoc} */ public function defined($option) { return ( array_key_exists($option, $this->options) || array_key_exists($option, $this->input) ); } /** * {@inheritdoc} */ public function __isset($option) { return ( array_key_exists($option, $this->options) || array_key_exists($option, $this->input) ) && $this->__get($option) !== null; } /** * {@inheritdoc} */ public function __get($option) { if (isset($this->options[$option]) || array_key_exists($option, $this->options)) { return $this->options[$option]; } if (isset($this->input[$option]) || array_key_exists($option, $this->input)) { $value = $this->input[$option]; unset($this->input[$option]); if (method_exists($value, '__invoke')) { $value = $value($this, $option); } if (isset($this->handlers[$option])) { $handler = $this->handlers[$option]; $handler = new $handler(); $value = $handler->filter($this, $value); } return $this->options[$option] = $value; } if (isset($this->handlers[$option])) { return $this->options[$option] = $this->getDefault($option); } return null; } } /** * Configures a connection factory used by the client to create new connection * instances for single Redis nodes. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ConnectionFactoryOption implements OptionInterface { /** * {@inheritdoc} */ public function filter(OptionsInterface $options, $value) { if ($value instanceof FactoryInterface) { return $value; } elseif (is_array($value)) { $factory = $this->getDefault($options); foreach ($value as $scheme => $initializer) { $factory->define($scheme, $initializer); } return $factory; } else { throw new InvalidArgumentException( 'Invalid value provided for the connections option.' ); } } /** * {@inheritdoc} */ public function getDefault(OptionsInterface $options) { return new Factory(); } } /** * Configures whether consumers (such as the client) should throw exceptions on * Redis errors (-ERR responses) or just return instances of error responses. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ExceptionsOption implements OptionInterface { /** * {@inheritdoc} */ public function filter(OptionsInterface $options, $value) { return filter_var($value, FILTER_VALIDATE_BOOLEAN); } /** * {@inheritdoc} */ public function getDefault(OptionsInterface $options) { return true; } } /** * Configures an aggregate connection used for clustering * multiple Redis nodes using various implementations with * different algorithms or strategies. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ClusterOption implements OptionInterface { /** * Creates a new cluster connection from on a known descriptive name. * * @param OptionsInterface $options Instance of the client options. * @param string $id Descriptive identifier of the cluster type (`predis`, `redis-cluster`) * * @return ClusterInterface|null */ protected function createByDescription(OptionsInterface $options, $id) { switch ($id) { case 'predis': case 'predis-cluster': return new PredisCluster(); case 'redis': case 'redis-cluster': return new RedisCluster($options->connections); default: return; } } /** * {@inheritdoc} */ public function filter(OptionsInterface $options, $value) { if (is_string($value)) { $value = $this->createByDescription($options, $value); } if (!$value instanceof ClusterInterface) { throw new InvalidArgumentException( "An instance of type 'Predis\Connection\Aggregate\ClusterInterface' was expected." ); } return $value; } /** * {@inheritdoc} */ public function getDefault(OptionsInterface $options) { return new PredisCluster(); } } /* --------------------------------------------------------------------------- */ namespace Predis\Response; use Predis\PredisException; /** * Represents a complex response object from Redis. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ResponseInterface { } /** * Represents an error returned by Redis (responses identified by "-" in the * Redis protocol) during the execution of an operation on the server. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ErrorInterface extends ResponseInterface { /** * Returns the error message * * @return string */ public function getMessage(); /** * Returns the error type (e.g. ERR, ASK, MOVED) * * @return string */ public function getErrorType(); } /** * Represents a status response returned by Redis. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Status implements ResponseInterface { private static $OK; private static $QUEUED; private $payload; /** * @param string $payload Payload of the status response as returned by Redis. */ public function __construct($payload) { $this->payload = $payload; } /** * Converts the response object to its string representation. * * @return string */ public function __toString() { return $this->payload; } /** * Returns the payload of status response. * * @return string */ public function getPayload() { return $this->payload; } /** * Returns an instance of a status response object. * * Common status responses such as OK or QUEUED are cached in order to lower * the global memory usage especially when using pipelines. * * @param string $payload Status response payload. * * @return string */ public static function get($payload) { switch ($payload) { case 'OK': case 'QUEUED': if (isset(self::$$payload)) { return self::$$payload; } return self::$$payload = new self($payload); default: return new self($payload); } } } /** * Represents an error returned by Redis (-ERR responses) during the execution * of a command on the server. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Error implements ErrorInterface { private $message; /** * @param string $message Error message returned by Redis */ public function __construct($message) { $this->message = $message; } /** * {@inheritdoc} */ public function getMessage() { return $this->message; } /** * {@inheritdoc} */ public function getErrorType() { list($errorType, ) = explode(' ', $this->getMessage(), 2); return $errorType; } /** * Converts the object to its string representation. * * @return string */ public function __toString() { return $this->getMessage(); } } /** * Exception class that identifies server-side Redis errors. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ServerException extends PredisException implements ErrorInterface { /** * Gets the type of the error returned by Redis. * * @return string */ public function getErrorType() { list($errorType, ) = explode(' ', $this->getMessage(), 2); return $errorType; } /** * Converts the exception to an instance of Predis\Response\Error. * * @return Error */ public function toErrorResponse() { return new Error($this->getMessage()); } } /* --------------------------------------------------------------------------- */ namespace Predis\Protocol\Text\Handler; use Predis\CommunicationException; use Predis\Connection\CompositeConnectionInterface; use Predis\Protocol\ProtocolException; use Predis\Response\Error; use Predis\Response\Status; use Predis\Response\Iterator\MultiBulk as MultiBulkIterator; /** * Defines a pluggable handler used to parse a particular type of response. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ResponseHandlerInterface { /** * Deserializes a response returned by Redis and reads more data from the * connection if needed. * * @param CompositeConnectionInterface $connection Redis connection. * @param string $payload String payload. * * @return mixed */ public function handle(CompositeConnectionInterface $connection, $payload); } /** * Handler for the status response type in the standard Redis wire protocol. It * translates certain classes of status response to PHP objects or just returns * the payload as a string. * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class StatusResponse implements ResponseHandlerInterface { /** * {@inheritdoc} */ public function handle(CompositeConnectionInterface $connection, $payload) { return Status::get($payload); } } /** * Handler for the multibulk response type in the standard Redis wire protocol. * It returns multibulk responses as iterators that can stream bulk elements. * * Streamable multibulk responses are not globally supported by the abstractions * built-in into Predis, such as transactions or pipelines. Use them with care! * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class StreamableMultiBulkResponse implements ResponseHandlerInterface { /** * {@inheritdoc} */ public function handle(CompositeConnectionInterface $connection, $payload) { $length = (int) $payload; if ("$length" != $payload) { CommunicationException::handle(new ProtocolException( $connection, "Cannot parse '$payload' as a valid length for a multi-bulk response." )); } return new MultiBulkIterator($connection, $length); } } /** * Handler for the multibulk response type in the standard Redis wire protocol. * It returns multibulk responses as PHP arrays. * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class MultiBulkResponse implements ResponseHandlerInterface { /** * {@inheritdoc} */ public function handle(CompositeConnectionInterface $connection, $payload) { $length = (int) $payload; if ("$length" !== $payload) { CommunicationException::handle(new ProtocolException( $connection, "Cannot parse '$payload' as a valid length of a multi-bulk response." )); } if ($length === -1) { return null; } $list = array(); if ($length > 0) { $handlersCache = array(); $reader = $connection->getProtocol()->getResponseReader(); for ($i = 0; $i < $length; $i++) { $header = $connection->readLine(); $prefix = $header[0]; if (isset($handlersCache[$prefix])) { $handler = $handlersCache[$prefix]; } else { $handler = $reader->getHandler($prefix); $handlersCache[$prefix] = $handler; } $list[$i] = $handler->handle($connection, substr($header, 1)); } } return $list; } } /** * Handler for the error response type in the standard Redis wire protocol. * It translates the payload to a complex response object for Predis. * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class ErrorResponse implements ResponseHandlerInterface { /** * {@inheritdoc} */ public function handle(CompositeConnectionInterface $connection, $payload) { return new Error($payload); } } /** * Handler for the integer response type in the standard Redis wire protocol. * It translates the payload an integer or NULL. * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class IntegerResponse implements ResponseHandlerInterface { /** * {@inheritdoc} */ public function handle(CompositeConnectionInterface $connection, $payload) { if (is_numeric($payload)) { return (int) $payload; } if ($payload !== 'nil') { CommunicationException::handle(new ProtocolException( $connection, "Cannot parse '$payload' as a valid numeric response." )); } return null; } } /** * Handler for the bulk response type in the standard Redis wire protocol. * It translates the payload to a string or a NULL. * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class BulkResponse implements ResponseHandlerInterface { /** * {@inheritdoc} */ public function handle(CompositeConnectionInterface $connection, $payload) { $length = (int) $payload; if ("$length" !== $payload) { CommunicationException::handle(new ProtocolException( $connection, "Cannot parse '$payload' as a valid length for a bulk response." )); } if ($length >= 0) { return substr($connection->readBuffer($length + 2), 0, -2); } if ($length == -1) { return null; } CommunicationException::handle(new ProtocolException( $connection, "Value '$payload' is not a valid length for a bulk response." )); return; } } /* --------------------------------------------------------------------------- */ namespace Predis\Collection\Iterator; use Iterator; use Predis\ClientInterface; use Predis\NotSupportedException; use InvalidArgumentException; /** * Provides the base implementation for a fully-rewindable PHP iterator that can * incrementally iterate over cursor-based collections stored on Redis using the * commands in the `SCAN` family. * * Given their incremental nature with multiple fetches, these kind of iterators * offer limited guarantees about the returned elements because the collection * can change several times during the iteration process. * * @see http://redis.io/commands/scan * * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class CursorBasedIterator implements Iterator { protected $client; protected $match; protected $count; protected $valid; protected $fetchmore; protected $elements; protected $cursor; protected $position; protected $current; /** * @param ClientInterface $client Client connected to Redis. * @param string $match Pattern to match during the server-side iteration. * @param int $count Hint used by Redis to compute the number of results per iteration. */ public function __construct(ClientInterface $client, $match = null, $count = null) { $this->client = $client; $this->match = $match; $this->count = $count; $this->reset(); } /** * Ensures that the client supports the specified Redis command required to * fetch elements from the server to perform the iteration. * * @param ClientInterface $client Client connected to Redis. * @param string $commandID Command ID. * * @throws NotSupportedException */ protected function requiredCommand(ClientInterface $client, $commandID) { if (!$client->getProfile()->supportsCommand($commandID)) { throw new NotSupportedException("The current profile does not support '$commandID'."); } } /** * Resets the inner state of the iterator. */ protected function reset() { $this->valid = true; $this->fetchmore = true; $this->elements = array(); $this->cursor = 0; $this->position = -1; $this->current = null; } /** * Returns an array of options for the `SCAN` command. * * @return array */ protected function getScanOptions() { $options = array(); if (strlen($this->match) > 0) { $options['MATCH'] = $this->match; } if ($this->count > 0) { $options['COUNT'] = $this->count; } return $options; } /** * Fetches a new set of elements from the remote collection, effectively * advancing the iteration process. * * @return array */ abstract protected function executeCommand(); /** * Populates the local buffer of elements fetched from the server during * the iteration. */ protected function fetch() { list($cursor, $elements) = $this->executeCommand(); if (!$cursor) { $this->fetchmore = false; } $this->cursor = $cursor; $this->elements = $elements; } /** * Extracts next values for key() and current(). */ protected function extractNext() { $this->position++; $this->current = array_shift($this->elements); } /** * {@inheritdoc} */ public function rewind() { $this->reset(); $this->next(); } /** * {@inheritdoc} */ public function current() { return $this->current; } /** * {@inheritdoc} */ public function key() { return $this->position; } /** * {@inheritdoc} */ public function next() { tryFetch: { if (!$this->elements && $this->fetchmore) { $this->fetch(); } if ($this->elements) { $this->extractNext(); } elseif ($this->cursor) { goto tryFetch; } else { $this->valid = false; } } } /** * {@inheritdoc} */ public function valid() { return $this->valid; } } /** * Abstracts the iteration of members stored in a sorted set by leveraging the * ZSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator. * * @author Daniele Alessandri <suppakilla@gmail.com> * @link http://redis.io/commands/scan */ class SortedSetKey extends CursorBasedIterator { protected $key; /** * {@inheritdoc} */ public function __construct(ClientInterface $client, $key, $match = null, $count = null) { $this->requiredCommand($client, 'ZSCAN'); parent::__construct($client, $match, $count); $this->key = $key; } /** * {@inheritdoc} */ protected function executeCommand() { return $this->client->zscan($this->key, $this->cursor, $this->getScanOptions()); } /** * {@inheritdoc} */ protected function extractNext() { if ($kv = each($this->elements)) { $this->position = $kv[0]; $this->current = $kv[1]; unset($this->elements[$this->position]); } } } /** * Abstracts the iteration of members stored in a set by leveraging the SSCAN * command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator. * * @author Daniele Alessandri <suppakilla@gmail.com> * @link http://redis.io/commands/scan */ class SetKey extends CursorBasedIterator { protected $key; /** * {@inheritdoc} */ public function __construct(ClientInterface $client, $key, $match = null, $count = null) { $this->requiredCommand($client, 'SSCAN'); parent::__construct($client, $match, $count); $this->key = $key; } /** * {@inheritdoc} */ protected function executeCommand() { return $this->client->sscan($this->key, $this->cursor, $this->getScanOptions()); } } /** * Abstracts the iteration of the keyspace on a Redis instance by leveraging the * SCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator. * * @author Daniele Alessandri <suppakilla@gmail.com> * @link http://redis.io/commands/scan */ class Keyspace extends CursorBasedIterator { /** * {@inheritdoc} */ public function __construct(ClientInterface $client, $match = null, $count = null) { $this->requiredCommand($client, 'SCAN'); parent::__construct($client, $match, $count); } /** * {@inheritdoc} */ protected function executeCommand() { return $this->client->scan($this->cursor, $this->getScanOptions()); } } /** * Abstracts the iteration of fields and values of an hash by leveraging the * HSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator. * * @author Daniele Alessandri <suppakilla@gmail.com> * @link http://redis.io/commands/scan */ class HashKey extends CursorBasedIterator { protected $key; /** * {@inheritdoc} */ public function __construct(ClientInterface $client, $key, $match = null, $count = null) { $this->requiredCommand($client, 'HSCAN'); parent::__construct($client, $match, $count); $this->key = $key; } /** * {@inheritdoc} */ protected function executeCommand() { return $this->client->hscan($this->key, $this->cursor, $this->getScanOptions()); } /** * {@inheritdoc} */ protected function extractNext() { $this->position = key($this->elements); $this->current = array_shift($this->elements); } } /** * Abstracts the iteration of items stored in a list by leveraging the LRANGE * command wrapped in a fully-rewindable PHP iterator. * * This iterator tries to emulate the behaviour of cursor-based iterators based * on the SCAN-family of commands introduced in Redis <= 2.8, meaning that due * to its incremental nature with multiple fetches it can only offer limited * guarantees on the returned elements because the collection can change several * times (trimmed, deleted, overwritten) during the iteration process. * * @author Daniele Alessandri <suppakilla@gmail.com> * @link http://redis.io/commands/lrange */ class ListKey implements Iterator { protected $client; protected $count; protected $key; protected $valid; protected $fetchmore; protected $elements; protected $position; protected $current; /** * @param ClientInterface $client Client connected to Redis. * @param string $key Redis list key. * @param int $count Number of items retrieved on each fetch operation. * * @throws \InvalidArgumentException */ public function __construct(ClientInterface $client, $key, $count = 10) { $this->requiredCommand($client, 'LRANGE'); if ((false === $count = filter_var($count, FILTER_VALIDATE_INT)) || $count < 0) { throw new InvalidArgumentException('The $count argument must be a positive integer.'); } $this->client = $client; $this->key = $key; $this->count = $count; $this->reset(); } /** * Ensures that the client instance supports the specified Redis command * required to fetch elements from the server to perform the iteration. * * @param ClientInterface $client Client connected to Redis. * @param string $commandID Command ID. * * @throws NotSupportedException */ protected function requiredCommand(ClientInterface $client, $commandID) { if (!$client->getProfile()->supportsCommand($commandID)) { throw new NotSupportedException("The current profile does not support '$commandID'."); } } /** * Resets the inner state of the iterator. */ protected function reset() { $this->valid = true; $this->fetchmore = true; $this->elements = array(); $this->position = -1; $this->current = null; } /** * Fetches a new set of elements from the remote collection, effectively * advancing the iteration process. * * @return array */ protected function executeCommand() { return $this->client->lrange($this->key, $this->position + 1, $this->position + $this->count); } /** * Populates the local buffer of elements fetched from the server during the * iteration. */ protected function fetch() { $elements = $this->executeCommand(); if (count($elements) < $this->count) { $this->fetchmore = false; } $this->elements = $elements; } /** * Extracts next values for key() and current(). */ protected function extractNext() { $this->position++; $this->current = array_shift($this->elements); } /** * {@inheritdoc} */ public function rewind() { $this->reset(); $this->next(); } /** * {@inheritdoc} */ public function current() { return $this->current; } /** * {@inheritdoc} */ public function key() { return $this->position; } /** * {@inheritdoc} */ public function next() { if (!$this->elements && $this->fetchmore) { $this->fetch(); } if ($this->elements) { $this->extractNext(); } else { $this->valid = false; } } /** * {@inheritdoc} */ public function valid() { return $this->valid; } } /* --------------------------------------------------------------------------- */ namespace Predis\Cluster; use InvalidArgumentException; use Predis\Command\CommandInterface; use Predis\Command\ScriptCommand; use Predis\Cluster\Distributor\DistributorInterface; use Predis\Cluster\Distributor\HashRing; use Predis\NotSupportedException; use Predis\Cluster\Hash\HashGeneratorInterface; use Predis\Cluster\Hash\CRC16; /** * Interface for classes defining the strategy used to calculate an hash out of * keys extracted from supported commands. * * This is mostly useful to support clustering via client-side sharding. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface StrategyInterface { /** * Returns a slot for the given command used for clustering distribution or * NULL when this is not possible. * * @param CommandInterface $command Command instance. * * @return int */ public function getSlot(CommandInterface $command); /** * Returns a slot for the given key used for clustering distribution or NULL * when this is not possible. * * @param string $key Key string. * * @return int */ public function getSlotByKey($key); /** * Returns a distributor instance to be used by the cluster. * * @return DistributorInterface */ public function getDistributor(); } /** * Common class implementing the logic needed to support clustering strategies. * * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class ClusterStrategy implements StrategyInterface { protected $commands; /** * */ public function __construct() { $this->commands = $this->getDefaultCommands(); } /** * Returns the default map of supported commands with their handlers. * * @return array */ protected function getDefaultCommands() { $getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument'); $getKeyFromAllArguments = array($this, 'getKeyFromAllArguments'); return array( /* commands operating on the key space */ 'EXISTS' => $getKeyFromFirstArgument, 'DEL' => $getKeyFromAllArguments, 'TYPE' => $getKeyFromFirstArgument, 'EXPIRE' => $getKeyFromFirstArgument, 'EXPIREAT' => $getKeyFromFirstArgument, 'PERSIST' => $getKeyFromFirstArgument, 'PEXPIRE' => $getKeyFromFirstArgument, 'PEXPIREAT' => $getKeyFromFirstArgument, 'TTL' => $getKeyFromFirstArgument, 'PTTL' => $getKeyFromFirstArgument, 'SORT' => $getKeyFromFirstArgument, // TODO 'DUMP' => $getKeyFromFirstArgument, 'RESTORE' => $getKeyFromFirstArgument, /* commands operating on string values */ 'APPEND' => $getKeyFromFirstArgument, 'DECR' => $getKeyFromFirstArgument, 'DECRBY' => $getKeyFromFirstArgument, 'GET' => $getKeyFromFirstArgument, 'GETBIT' => $getKeyFromFirstArgument, 'MGET' => $getKeyFromAllArguments, 'SET' => $getKeyFromFirstArgument, 'GETRANGE' => $getKeyFromFirstArgument, 'GETSET' => $getKeyFromFirstArgument, 'INCR' => $getKeyFromFirstArgument, 'INCRBY' => $getKeyFromFirstArgument, 'INCRBYFLOAT' => $getKeyFromFirstArgument, 'SETBIT' => $getKeyFromFirstArgument, 'SETEX' => $getKeyFromFirstArgument, 'MSET' => array($this, 'getKeyFromInterleavedArguments'), 'MSETNX' => array($this, 'getKeyFromInterleavedArguments'), 'SETNX' => $getKeyFromFirstArgument, 'SETRANGE' => $getKeyFromFirstArgument, 'STRLEN' => $getKeyFromFirstArgument, 'SUBSTR' => $getKeyFromFirstArgument, 'BITOP' => array($this, 'getKeyFromBitOp'), 'BITCOUNT' => $getKeyFromFirstArgument, /* commands operating on lists */ 'LINSERT' => $getKeyFromFirstArgument, 'LINDEX' => $getKeyFromFirstArgument, 'LLEN' => $getKeyFromFirstArgument, 'LPOP' => $getKeyFromFirstArgument, 'RPOP' => $getKeyFromFirstArgument, 'RPOPLPUSH' => $getKeyFromAllArguments, 'BLPOP' => array($this, 'getKeyFromBlockingListCommands'), 'BRPOP' => array($this, 'getKeyFromBlockingListCommands'), 'BRPOPLPUSH' => array($this, 'getKeyFromBlockingListCommands'), 'LPUSH' => $getKeyFromFirstArgument, 'LPUSHX' => $getKeyFromFirstArgument, 'RPUSH' => $getKeyFromFirstArgument, 'RPUSHX' => $getKeyFromFirstArgument, 'LRANGE' => $getKeyFromFirstArgument, 'LREM' => $getKeyFromFirstArgument, 'LSET' => $getKeyFromFirstArgument, 'LTRIM' => $getKeyFromFirstArgument, /* commands operating on sets */ 'SADD' => $getKeyFromFirstArgument, 'SCARD' => $getKeyFromFirstArgument, 'SDIFF' => $getKeyFromAllArguments, 'SDIFFSTORE' => $getKeyFromAllArguments, 'SINTER' => $getKeyFromAllArguments, 'SINTERSTORE' => $getKeyFromAllArguments, 'SUNION' => $getKeyFromAllArguments, 'SUNIONSTORE' => $getKeyFromAllArguments, 'SISMEMBER' => $getKeyFromFirstArgument, 'SMEMBERS' => $getKeyFromFirstArgument, 'SSCAN' => $getKeyFromFirstArgument, 'SPOP' => $getKeyFromFirstArgument, 'SRANDMEMBER' => $getKeyFromFirstArgument, 'SREM' => $getKeyFromFirstArgument, /* commands operating on sorted sets */ 'ZADD' => $getKeyFromFirstArgument, 'ZCARD' => $getKeyFromFirstArgument, 'ZCOUNT' => $getKeyFromFirstArgument, 'ZINCRBY' => $getKeyFromFirstArgument, 'ZINTERSTORE' => array($this, 'getKeyFromZsetAggregationCommands'), 'ZRANGE' => $getKeyFromFirstArgument, 'ZRANGEBYSCORE' => $getKeyFromFirstArgument, 'ZRANK' => $getKeyFromFirstArgument, 'ZREM' => $getKeyFromFirstArgument, 'ZREMRANGEBYRANK' => $getKeyFromFirstArgument, 'ZREMRANGEBYSCORE' => $getKeyFromFirstArgument, 'ZREVRANGE' => $getKeyFromFirstArgument, 'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument, 'ZREVRANK' => $getKeyFromFirstArgument, 'ZSCORE' => $getKeyFromFirstArgument, 'ZUNIONSTORE' => array($this, 'getKeyFromZsetAggregationCommands'), 'ZSCAN' => $getKeyFromFirstArgument, 'ZLEXCOUNT' => $getKeyFromFirstArgument, 'ZRANGEBYLEX' => $getKeyFromFirstArgument, 'ZREMRANGEBYLEX' => $getKeyFromFirstArgument, /* commands operating on hashes */ 'HDEL' => $getKeyFromFirstArgument, 'HEXISTS' => $getKeyFromFirstArgument, 'HGET' => $getKeyFromFirstArgument, 'HGETALL' => $getKeyFromFirstArgument, 'HMGET' => $getKeyFromFirstArgument, 'HMSET' => $getKeyFromFirstArgument, 'HINCRBY' => $getKeyFromFirstArgument, 'HINCRBYFLOAT' => $getKeyFromFirstArgument, 'HKEYS' => $getKeyFromFirstArgument, 'HLEN' => $getKeyFromFirstArgument, 'HSET' => $getKeyFromFirstArgument, 'HSETNX' => $getKeyFromFirstArgument, 'HVALS' => $getKeyFromFirstArgument, 'HSCAN' => $getKeyFromFirstArgument, /* commands operating on HyperLogLog */ 'PFADD' => $getKeyFromFirstArgument, 'PFCOUNT' => $getKeyFromAllArguments, 'PFMERGE' => $getKeyFromAllArguments, /* scripting */ 'EVAL' => array($this, 'getKeyFromScriptingCommands'), 'EVALSHA' => array($this, 'getKeyFromScriptingCommands'), ); } /** * Returns the list of IDs for the supported commands. * * @return array */ public function getSupportedCommands() { return array_keys($this->commands); } /** * Sets an handler for the specified command ID. * * The signature of the callback must have a single parameter of type * Predis\Command\CommandInterface. * * When the callback argument is omitted or NULL, the previously associated * handler for the specified command ID is removed. * * @param string $commandID Command ID. * @param mixed $callback A valid callable object, or NULL to unset the handler. * * @throws \InvalidArgumentException */ public function setCommandHandler($commandID, $callback = null) { $commandID = strtoupper($commandID); if (!isset($callback)) { unset($this->commands[$commandID]); return; } if (!is_callable($callback)) { throw new InvalidArgumentException( "The argument must be a callable object or NULL." ); } $this->commands[$commandID] = $callback; } /** * Extracts the key from the first argument of a command instance. * * @param CommandInterface $command Command instance. * * @return string */ protected function getKeyFromFirstArgument(CommandInterface $command) { return $command->getArgument(0); } /** * Extracts the key from a command with multiple keys only when all keys in * the arguments array produce the same hash. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromAllArguments(CommandInterface $command) { $arguments = $command->getArguments(); if ($this->checkSameSlotForKeys($arguments)) { return $arguments[0]; } } /** * Extracts the key from a command with multiple keys only when all keys in * the arguments array produce the same hash. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromInterleavedArguments(CommandInterface $command) { $arguments = $command->getArguments(); $keys = array(); for ($i = 0; $i < count($arguments); $i += 2) { $keys[] = $arguments[$i]; } if ($this->checkSameSlotForKeys($keys)) { return $arguments[0]; } } /** * Extracts the key from BLPOP and BRPOP commands. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromBlockingListCommands(CommandInterface $command) { $arguments = $command->getArguments(); if ($this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) { return $arguments[0]; } } /** * Extracts the key from BITOP command. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromBitOp(CommandInterface $command) { $arguments = $command->getArguments(); if ($this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) { return $arguments[1]; } } /** * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromZsetAggregationCommands(CommandInterface $command) { $arguments = $command->getArguments(); $keys = array_merge(array($arguments[0]), array_slice($arguments, 2, $arguments[1])); if ($this->checkSameSlotForKeys($keys)) { return $arguments[0]; } } /** * Extracts the key from EVAL and EVALSHA commands. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromScriptingCommands(CommandInterface $command) { if ($command instanceof ScriptCommand) { $keys = $command->getKeys(); } else { $keys = array_slice($args = $command->getArguments(), 2, $args[1]); } if ($keys && $this->checkSameSlotForKeys($keys)) { return $keys[0]; } } /** * {@inheritdoc} */ public function getSlot(CommandInterface $command) { $slot = $command->getSlot(); if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) { $key = call_user_func($this->commands[$cmdID], $command); if (isset($key)) { $slot = $this->getSlotByKey($key); $command->setSlot($slot); } } return $slot; } /** * Checks if the specified array of keys will generate the same hash. * * @param array $keys Array of keys. * * @return bool */ protected function checkSameSlotForKeys(array $keys) { if (!$count = count($keys)) { return false; } $currentSlot = $this->getSlotByKey($keys[0]); for ($i = 1; $i < $count; $i++) { $nextSlot = $this->getSlotByKey($keys[$i]); if ($currentSlot !== $nextSlot) { return false; } $currentSlot = $nextSlot; } return true; } /** * Returns only the hashable part of a key (delimited by "{...}"), or the * whole key if a key tag is not found in the string. * * @param string $key A key. * * @return string */ protected function extractKeyTag($key) { if (false !== $start = strpos($key, '{')) { if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) { $key = substr($key, $start, $end - $start); } } return $key; } } /** * Default cluster strategy used by Predis to handle client-side sharding. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class PredisStrategy extends ClusterStrategy { protected $distributor; /** * @param DistributorInterface $distributor Optional distributor instance. */ public function __construct(DistributorInterface $distributor = null) { parent::__construct(); $this->distributor = $distributor ?: new HashRing(); } /** * {@inheritdoc} */ public function getSlotByKey($key) { $key = $this->extractKeyTag($key); $hash = $this->distributor->hash($key); $slot = $this->distributor->getSlot($hash); return $slot; } /** * {@inheritdoc} */ protected function checkSameSlotForKeys(array $keys) { if (!$count = count($keys)) { return false; } $currentKey = $this->extractKeyTag($keys[0]); for ($i = 1; $i < $count; $i++) { $nextKey = $this->extractKeyTag($keys[$i]); if ($currentKey !== $nextKey) { return false; } $currentKey = $nextKey; } return true; } /** * {@inheritdoc} */ public function getDistributor() { return $this->distributor; } } /** * Default class used by Predis to calculate hashes out of keys of * commands supported by redis-cluster. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisStrategy extends ClusterStrategy { protected $hashGenerator; /** * @param HashGeneratorInterface $hashGenerator Hash generator instance. */ public function __construct(HashGeneratorInterface $hashGenerator = null) { parent::__construct(); $this->hashGenerator = $hashGenerator ?: new CRC16(); } /** * {@inheritdoc} */ public function getSlotByKey($key) { $key = $this->extractKeyTag($key); $slot = $this->hashGenerator->hash($key) & 0x3FFF; return $slot; } /** * {@inheritdoc} */ public function getDistributor() { throw new NotSupportedException( 'This cluster strategy does not provide an external distributor' ); } } /* --------------------------------------------------------------------------- */ namespace Predis\Protocol; use Predis\CommunicationException; use Predis\Command\CommandInterface; use Predis\Connection\CompositeConnectionInterface; /** * Defines a pluggable protocol processor capable of serializing commands and * deserializing responses into PHP objects directly from a connection. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ProtocolProcessorInterface { /** * Writes a request over a connection to Redis. * * @param CompositeConnectionInterface $connection Redis connection. * @param CommandInterface $command Command instance. */ public function write(CompositeConnectionInterface $connection, CommandInterface $command); /** * Reads a response from a connection to Redis. * * @param CompositeConnectionInterface $connection Redis connection. * * @return mixed */ public function read(CompositeConnectionInterface $connection); } /** * Defines a pluggable reader capable of parsing responses returned by Redis and * deserializing them to PHP objects. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ResponseReaderInterface { /** * Reads a response from a connection to Redis. * * @param CompositeConnectionInterface $connection Redis connection. * * @return mixed */ public function read(CompositeConnectionInterface $connection); } /** * Defines a pluggable serializer for Redis commands. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface RequestSerializerInterface { /** * Serializes a Redis command. * * @param CommandInterface $command Redis command. * * @return string */ public function serialize(CommandInterface $command); } /** * Exception used to indentify errors encountered while parsing the Redis wire * protocol. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ProtocolException extends CommunicationException { } /* --------------------------------------------------------------------------- */ namespace Predis\Connection\Aggregate; use Predis\Connection\AggregateConnectionInterface; use InvalidArgumentException; use RuntimeException; use Predis\Command\CommandInterface; use Predis\Connection\NodeConnectionInterface; use Predis\Replication\ReplicationStrategy; use ArrayIterator; use Countable; use IteratorAggregate; use Predis\NotSupportedException; use Predis\Cluster\PredisStrategy; use Predis\Cluster\StrategyInterface; use OutOfBoundsException; use Predis\Cluster\RedisStrategy as RedisClusterStrategy; use Predis\Command\RawCommand; use Predis\Connection\FactoryInterface; use Predis\Response\ErrorInterface as ErrorResponseInterface; /** * Defines a cluster of Redis servers formed by aggregating multiple connection * instances to single Redis nodes. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ClusterInterface extends AggregateConnectionInterface { } /** * Defines a group of Redis nodes in a master / slave replication setup. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ReplicationInterface extends AggregateConnectionInterface { /** * Switches the internal connection instance in use. * * @param string $connection Alias of a connection */ public function switchTo($connection); /** * Returns the connection instance currently in use by the aggregate * connection. * * @return NodeConnectionInterface */ public function getCurrent(); /** * Returns the connection instance for the master Redis node. * * @return NodeConnectionInterface */ public function getMaster(); /** * Returns a list of connection instances to slave nodes. * * @return NodeConnectionInterface */ public function getSlaves(); } /** * Abstraction for a Redis-backed cluster of nodes (Redis >= 3.0.0). * * This connection backend offers smart support for redis-cluster by handling * automatic slots map (re)generation upon -MOVED or -ASK responses returned by * Redis when redirecting a client to a different node. * * The cluster can be pre-initialized using only a subset of the actual nodes in * the cluster, Predis will do the rest by adjusting the slots map and creating * the missing underlying connection instances on the fly. * * It is possible to pre-associate connections to a slots range with the "slots" * parameter in the form "$first-$last". This can greatly reduce runtime node * guessing and redirections. * * It is also possible to ask for the full and updated slots map directly to one * of the nodes and optionally enable such a behaviour upon -MOVED redirections. * Asking for the cluster configuration to Redis is actually done by issuing a * CLUSTER SLOTS command to a random node in the pool. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisCluster implements ClusterInterface, IteratorAggregate, Countable { private $useClusterSlots = true; private $defaultParameters = array(); private $pool = array(); private $slots = array(); private $slotsMap; private $strategy; private $connections; /** * @param FactoryInterface $connections Optional connection factory. * @param StrategyInterface $strategy Optional cluster strategy. */ public function __construct( FactoryInterface $connections, StrategyInterface $strategy = null ) { $this->connections = $connections; $this->strategy = $strategy ?: new RedisClusterStrategy(); } /** * {@inheritdoc} */ public function isConnected() { foreach ($this->pool as $connection) { if ($connection->isConnected()) { return true; } } return false; } /** * {@inheritdoc} */ public function connect() { if ($connection = $this->getRandomConnection()) { $connection->connect(); } } /** * {@inheritdoc} */ public function disconnect() { foreach ($this->pool as $connection) { $connection->disconnect(); } } /** * {@inheritdoc} */ public function add(NodeConnectionInterface $connection) { $this->pool[(string) $connection] = $connection; unset($this->slotsMap); } /** * {@inheritdoc} */ public function remove(NodeConnectionInterface $connection) { if (false !== $id = array_search($connection, $this->pool, true)) { unset( $this->pool[$id], $this->slotsMap ); return true; } return false; } /** * Removes a connection instance by using its identifier. * * @param string $connectionID Connection identifier. * * @return bool True if the connection was in the pool. */ public function removeById($connectionID) { if (isset($this->pool[$connectionID])) { unset( $this->pool[$connectionID], $this->slotsMap ); return true; } return false; } /** * Generates the current slots map by guessing the cluster configuration out * of the connection parameters of the connections in the pool. * * Generation is based on the same algorithm used by Redis to generate the * cluster, so it is most effective when all of the connections supplied on * initialization have the "slots" parameter properly set accordingly to the * current cluster configuration. */ public function buildSlotsMap() { $this->slotsMap = array(); foreach ($this->pool as $connectionID => $connection) { $parameters = $connection->getParameters(); if (!isset($parameters->slots)) { continue; } $slots = explode('-', $parameters->slots, 2); $this->setSlots($slots[0], $slots[1], $connectionID); } } /** * Generates an updated slots map fetching the cluster configuration using * the CLUSTER SLOTS command against the specified node or a random one from * the pool. * * @param NodeConnectionInterface $connection Optional connection instance. * * @return array */ public function askSlotsMap(NodeConnectionInterface $connection = null) { if (!$connection && !$connection = $this->getRandomConnection()) { return array(); } $command = RawCommand::create('CLUSTER', 'SLOTS'); $response = $connection->executeCommand($command); foreach ($response as $slots) { // We only support master servers for now, so we ignore subsequent // elements in the $slots array identifying slaves. list($start, $end, $master) = $slots; if ($master[0] === '') { $this->setSlots($start, $end, (string) $connection); } else { $this->setSlots($start, $end, "{$master[0]}:{$master[1]}"); } } return $this->slotsMap; } /** * Returns the current slots map for the cluster. * * @return array */ public function getSlotsMap() { if (!isset($this->slotsMap)) { $this->slotsMap = array(); } return $this->slotsMap; } /** * Pre-associates a connection to a slots range to avoid runtime guessing. * * @param int $first Initial slot of the range. * @param int $last Last slot of the range. * @param NodeConnectionInterface|string $connection ID or connection instance. * * @throws \OutOfBoundsException */ public function setSlots($first, $last, $connection) { if ($first < 0x0000 || $first > 0x3FFF || $last < 0x0000 || $last > 0x3FFF || $last < $first ) { throw new OutOfBoundsException( "Invalid slot range for $connection: [$first-$last]." ); } $slots = array_fill($first, $last - $first + 1, (string) $connection); $this->slotsMap = $this->getSlotsMap() + $slots; } /** * Guesses the correct node associated to a given slot using a precalculated * slots map, falling back to the same logic used by Redis to initialize a * cluster (best-effort). * * @param int $slot Slot index. * * @return string Connection ID. */ protected function guessNode($slot) { if (!isset($this->slotsMap)) { $this->buildSlotsMap(); } if (isset($this->slotsMap[$slot])) { return $this->slotsMap[$slot]; } $count = count($this->pool); $index = min((int) ($slot / (int) (16384 / $count)), $count - 1); $nodes = array_keys($this->pool); return $nodes[$index]; } /** * Creates a new connection instance from the given connection ID. * * @param string $connectionID Identifier for the connection. * * @return NodeConnectionInterface */ protected function createConnection($connectionID) { $host = explode(':', $connectionID, 2); $parameters = array_merge($this->defaultParameters, array( 'host' => $host[0], 'port' => $host[1], )); $connection = $this->connections->create($parameters); return $connection; } /** * {@inheritdoc} */ public function getConnection(CommandInterface $command) { $slot = $this->strategy->getSlot($command); if (!isset($slot)) { throw new NotSupportedException( "Cannot use '{$command->getId()}' with redis-cluster." ); } if (isset($this->slots[$slot])) { return $this->slots[$slot]; } else { return $this->getConnectionBySlot($slot); } } /** * Returns the connection currently associated to a given slot. * * @param int $slot Slot index. * * @return NodeConnectionInterface * * @throws \OutOfBoundsException */ public function getConnectionBySlot($slot) { if ($slot < 0x0000 || $slot > 0x3FFF) { throw new OutOfBoundsException("Invalid slot [$slot]."); } if (isset($this->slots[$slot])) { return $this->slots[$slot]; } $connectionID = $this->guessNode($slot); if (!$connection = $this->getConnectionById($connectionID)) { $connection = $this->createConnection($connectionID); $this->pool[$connectionID] = $connection; } return $this->slots[$slot] = $connection; } /** * {@inheritdoc} */ public function getConnectionById($connectionID) { if (isset($this->pool[$connectionID])) { return $this->pool[$connectionID]; } } /** * Returns a random connection from the pool. * * @return NodeConnectionInterface|null */ protected function getRandomConnection() { if ($this->pool) { return $this->pool[array_rand($this->pool)]; } } /** * Permanently associates the connection instance to a new slot. * The connection is added to the connections pool if not yet included. * * @param NodeConnectionInterface $connection Connection instance. * @param int $slot Target slot index. */ protected function move(NodeConnectionInterface $connection, $slot) { $this->pool[(string) $connection] = $connection; $this->slots[(int) $slot] = $connection; } /** * Handles -ERR responses returned by Redis. * * @param CommandInterface $command Command that generated the -ERR response. * @param ErrorResponseInterface $error Redis error response object. * * @return mixed */ protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $error) { $details = explode(' ', $error->getMessage(), 2); switch ($details[0]) { case 'MOVED': return $this->onMovedResponse($command, $details[1]); case 'ASK': return $this->onAskResponse($command, $details[1]); default: return $error; } } /** * Handles -MOVED responses by executing again the command against the node * indicated by the Redis response. * * @param CommandInterface $command Command that generated the -MOVED response. * @param string $details Parameters of the -MOVED response. * * @return mixed */ protected function onMovedResponse(CommandInterface $command, $details) { list($slot, $connectionID) = explode(' ', $details, 2); if (!$connection = $this->getConnectionById($connectionID)) { $connection = $this->createConnection($connectionID); } if ($this->useClusterSlots) { $this->askSlotsMap($connection); } $this->move($connection, $slot); $response = $this->executeCommand($command); return $response; } /** * Handles -ASK responses by executing again the command against the node * indicated by the Redis response. * * @param CommandInterface $command Command that generated the -ASK response. * @param string $details Parameters of the -ASK response. * @return mixed */ protected function onAskResponse(CommandInterface $command, $details) { list($slot, $connectionID) = explode(' ', $details, 2); if (!$connection = $this->getConnectionById($connectionID)) { $connection = $this->createConnection($connectionID); } $connection->executeCommand(RawCommand::create('ASKING')); $response = $connection->executeCommand($command); return $response; } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $this->getConnection($command)->writeRequest($command); } /** * {@inheritdoc} */ public function readResponse(CommandInterface $command) { return $this->getConnection($command)->readResponse($command); } /** * {@inheritdoc} */ public function executeCommand(CommandInterface $command) { $connection = $this->getConnection($command); $response = $connection->executeCommand($command); if ($response instanceof ErrorResponseInterface) { return $this->onErrorResponse($command, $response); } return $response; } /** * {@inheritdoc} */ public function count() { return count($this->pool); } /** * {@inheritdoc} */ public function getIterator() { return new ArrayIterator(array_values($this->pool)); } /** * Returns the underlying command hash strategy used to hash commands by * using keys found in their arguments. * * @return StrategyInterface */ public function getClusterStrategy() { return $this->strategy; } /** * Returns the underlying connection factory used to create new connection * instances to Redis nodes indicated by redis-cluster. * * @return FactoryInterface */ public function getConnectionFactory() { return $this->connections; } /** * Enables automatic fetching of the current slots map from one of the nodes * using the CLUSTER SLOTS command. This option is disabled by default but * asking the current slots map to Redis upon -MOVED responses may reduce * overhead by eliminating the trial-and-error nature of the node guessing * procedure, mostly when targeting many keys that would end up in a lot of * redirections. * * The slots map can still be manually fetched using the askSlotsMap() * method whether or not this option is enabled. * * @param bool $value Enable or disable the use of CLUSTER SLOTS. */ public function useClusterSlots($value) { $this->useClusterSlots = (bool) $value; } /** * Sets a default array of connection parameters to be applied when creating * new connection instances on the fly when they are not part of the initial * pool supplied upon cluster initialization. * * These parameters are not applied to connections added to the pool using * the add() method. * * @param array $parameters Array of connection parameters. */ public function setDefaultParameters(array $parameters) { $this->defaultParameters = array_merge( $this->defaultParameters, $parameters ?: array() ); } } /** * Abstraction for a cluster of aggregate connections to various Redis servers * implementing client-side sharding based on pluggable distribution strategies. * * @author Daniele Alessandri <suppakilla@gmail.com> * @todo Add the ability to remove connections from pool. */ class PredisCluster implements ClusterInterface, IteratorAggregate, Countable { private $pool; private $strategy; private $distributor; /** * @param StrategyInterface $strategy Optional cluster strategy. */ public function __construct(StrategyInterface $strategy = null) { $this->pool = array(); $this->strategy = $strategy ?: new PredisStrategy(); $this->distributor = $this->strategy->getDistributor(); } /** * {@inheritdoc} */ public function isConnected() { foreach ($this->pool as $connection) { if ($connection->isConnected()) { return true; } } return false; } /** * {@inheritdoc} */ public function connect() { foreach ($this->pool as $connection) { $connection->connect(); } } /** * {@inheritdoc} */ public function disconnect() { foreach ($this->pool as $connection) { $connection->disconnect(); } } /** * {@inheritdoc} */ public function add(NodeConnectionInterface $connection) { $parameters = $connection->getParameters(); if (isset($parameters->alias)) { $this->pool[$parameters->alias] = $connection; } else { $this->pool[] = $connection; } $weight = isset($parameters->weight) ? $parameters->weight : null; $this->distributor->add($connection, $weight); } /** * {@inheritdoc} */ public function remove(NodeConnectionInterface $connection) { if (($id = array_search($connection, $this->pool, true)) !== false) { unset($this->pool[$id]); $this->distributor->remove($connection); return true; } return false; } /** * Removes a connection instance using its alias or index. * * @param string $connectionID Alias or index of a connection. * * @return bool Returns true if the connection was in the pool. */ public function removeById($connectionID) { if ($connection = $this->getConnectionById($connectionID)) { return $this->remove($connection); } return false; } /** * {@inheritdoc} */ public function getConnection(CommandInterface $command) { $slot = $this->strategy->getSlot($command); if (!isset($slot)) { throw new NotSupportedException( "Cannot use '{$command->getId()}' over clusters of connections." ); } $node = $this->distributor->getBySlot($slot); return $node; } /** * {@inheritdoc} */ public function getConnectionById($connectionID) { return isset($this->pool[$connectionID]) ? $this->pool[$connectionID] : null; } /** * Retrieves a connection instance from the cluster using a key. * * @param string $key Key string. * * @return NodeConnectionInterface */ public function getConnectionByKey($key) { $hash = $this->strategy->getSlotByKey($key); $node = $this->distributor->getBySlot($hash); return $node; } /** * Returns the underlying command hash strategy used to hash commands by * using keys found in their arguments. * * @return StrategyInterface */ public function getClusterStrategy() { return $this->strategy; } /** * {@inheritdoc} */ public function count() { return count($this->pool); } /** * {@inheritdoc} */ public function getIterator() { return new ArrayIterator($this->pool); } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $this->getConnection($command)->writeRequest($command); } /** * {@inheritdoc} */ public function readResponse(CommandInterface $command) { return $this->getConnection($command)->readResponse($command); } /** * {@inheritdoc} */ public function executeCommand(CommandInterface $command) { return $this->getConnection($command)->executeCommand($command); } /** * Executes the specified Redis command on all the nodes of a cluster. * * @param CommandInterface $command A Redis command. * * @return array */ public function executeCommandOnNodes(CommandInterface $command) { $responses = array(); foreach ($this->pool as $connection) { $responses[] = $connection->executeCommand($command); } return $responses; } } /** * Aggregate connection handling replication of Redis nodes configured in a * single master / multiple slaves setup. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class MasterSlaveReplication implements ReplicationInterface { protected $strategy; protected $master; protected $slaves; protected $current; /** * {@inheritdoc} */ public function __construct(ReplicationStrategy $strategy = null) { $this->slaves = array(); $this->strategy = $strategy ?: new ReplicationStrategy(); } /** * Checks if one master and at least one slave have been defined. */ protected function check() { if (!isset($this->master) || !$this->slaves) { throw new RuntimeException('Replication needs one master and at least one slave.'); } } /** * Resets the connection state. */ protected function reset() { $this->current = null; } /** * {@inheritdoc} */ public function add(NodeConnectionInterface $connection) { $alias = $connection->getParameters()->alias; if ($alias === 'master') { $this->master = $connection; } else { $this->slaves[$alias ?: count($this->slaves)] = $connection; } $this->reset(); } /** * {@inheritdoc} */ public function remove(NodeConnectionInterface $connection) { if ($connection->getParameters()->alias === 'master') { $this->master = null; $this->reset(); return true; } else { if (($id = array_search($connection, $this->slaves, true)) !== false) { unset($this->slaves[$id]); $this->reset(); return true; } } return false; } /** * {@inheritdoc} */ public function getConnection(CommandInterface $command) { if ($this->current === null) { $this->check(); $this->current = $this->strategy->isReadOperation($command) ? $this->pickSlave() : $this->master; return $this->current; } if ($this->current === $this->master) { return $this->current; } if (!$this->strategy->isReadOperation($command)) { $this->current = $this->master; } return $this->current; } /** * {@inheritdoc} */ public function getConnectionById($connectionId) { if ($connectionId === 'master') { return $this->master; } if (isset($this->slaves[$connectionId])) { return $this->slaves[$connectionId]; } return null; } /** * {@inheritdoc} */ public function switchTo($connection) { $this->check(); if (!$connection instanceof NodeConnectionInterface) { $connection = $this->getConnectionById($connection); } if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) { throw new InvalidArgumentException('Invalid connection or connection not found.'); } $this->current = $connection; } /** * {@inheritdoc} */ public function getCurrent() { return $this->current; } /** * {@inheritdoc} */ public function getMaster() { return $this->master; } /** * {@inheritdoc} */ public function getSlaves() { return array_values($this->slaves); } /** * Returns the underlying replication strategy. * * @return ReplicationStrategy */ public function getReplicationStrategy() { return $this->strategy; } /** * Returns a random slave. * * @return NodeConnectionInterface */ protected function pickSlave() { return $this->slaves[array_rand($this->slaves)]; } /** * {@inheritdoc} */ public function isConnected() { return $this->current ? $this->current->isConnected() : false; } /** * {@inheritdoc} */ public function connect() { if ($this->current === null) { $this->check(); $this->current = $this->pickSlave(); } $this->current->connect(); } /** * {@inheritdoc} */ public function disconnect() { if ($this->master) { $this->master->disconnect(); } foreach ($this->slaves as $connection) { $connection->disconnect(); } } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $this->getConnection($command)->writeRequest($command); } /** * {@inheritdoc} */ public function readResponse(CommandInterface $command) { return $this->getConnection($command)->readResponse($command); } /** * {@inheritdoc} */ public function executeCommand(CommandInterface $command) { return $this->getConnection($command)->executeCommand($command); } /** * {@inheritdoc} */ public function __sleep() { return array('master', 'slaves', 'strategy'); } } /* --------------------------------------------------------------------------- */ namespace Predis\Pipeline; use SplQueue; use Predis\ClientException; use Predis\ClientInterface; use Predis\Connection\ConnectionInterface; use Predis\Connection\NodeConnectionInterface; use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\ResponseInterface; use Predis\Response\ServerException; use Predis\NotSupportedException; use Predis\CommunicationException; use Predis\Connection\Aggregate\ClusterInterface; use Exception; use InvalidArgumentException; use Predis\ClientContextInterface; use Predis\Command\CommandInterface; use Predis\Connection\Aggregate\ReplicationInterface; /** * Implementation of a command pipeline in which write and read operations of * Redis commands are pipelined to alleviate the effects of network round-trips. * * {@inheritdoc} * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Pipeline implements ClientContextInterface { private $client; private $pipeline; private $responses = array(); private $running = false; /** * @param ClientInterface $client Client instance used by the context. */ public function __construct(ClientInterface $client) { $this->client = $client; $this->pipeline = new SplQueue(); } /** * Queues a command into the pipeline buffer. * * @param string $method Command ID. * @param array $arguments Arguments for the command. * * @return $this */ public function __call($method, $arguments) { $command = $this->client->createCommand($method, $arguments); $this->recordCommand($command); return $this; } /** * Queues a command instance into the pipeline buffer. * * @param CommandInterface $command Command to be queued in the buffer. */ protected function recordCommand(CommandInterface $command) { $this->pipeline->enqueue($command); } /** * Queues a command instance into the pipeline buffer. * * @param CommandInterface $command Command instance to be queued in the buffer. * * @return $this */ public function executeCommand(CommandInterface $command) { $this->recordCommand($command); return $this; } /** * Throws an exception on -ERR responses returned by Redis. * * @param ConnectionInterface $connection Redis connection that returned the error. * @param ErrorResponseInterface $response Instance of the error response. * * @throws ServerException */ protected function exception(ConnectionInterface $connection, ErrorResponseInterface $response) { $connection->disconnect(); $message = $response->getMessage(); throw new ServerException($message); } /** * Returns the underlying connection to be used by the pipeline. * * @return ConnectionInterface */ protected function getConnection() { $connection = $this->getClient()->getConnection(); if ($connection instanceof ReplicationInterface) { $connection->switchTo('master'); } return $connection; } /** * Implements the logic to flush the queued commands and read the responses * from the current connection. * * @param ConnectionInterface $connection Current connection instance. * @param SplQueue $commands Queued commands. * * @return array */ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { foreach ($commands as $command) { $connection->writeRequest($command); } $responses = array(); $exceptions = $this->throwServerExceptions(); while (!$commands->isEmpty()) { $command = $commands->dequeue(); $response = $connection->readResponse($command); if (!$response instanceof ResponseInterface) { $responses[] = $command->parseResponse($response); } elseif ($response instanceof ErrorResponseInterface && $exceptions) { $this->exception($connection, $response); } else { $responses[] = $response; } } return $responses; } /** * Flushes the buffer holding all of the commands queued so far. * * @param bool $send Specifies if the commands in the buffer should be sent to Redis. * * @return $this */ public function flushPipeline($send = true) { if ($send && !$this->pipeline->isEmpty()) { $responses = $this->executePipeline($this->getConnection(), $this->pipeline); $this->responses = array_merge($this->responses, $responses); } else { $this->pipeline = new SplQueue(); } return $this; } /** * Marks the running status of the pipeline. * * @param bool $bool Sets the running status of the pipeline. * * @throws ClientException */ private function setRunning($bool) { if ($bool && $this->running) { throw new ClientException('The current pipeline context is already being executed.'); } $this->running = $bool; } /** * Handles the actual execution of the whole pipeline. * * @param mixed $callable Optional callback for execution. * * @return array * * @throws Exception * @throws InvalidArgumentException */ public function execute($callable = null) { if ($callable && !is_callable($callable)) { throw new InvalidArgumentException('The argument must be a callable object.'); } $exception = null; $this->setRunning(true); try { if ($callable) { call_user_func($callable, $this); } $this->flushPipeline(); } catch (Exception $exception) { // NOOP } $this->setRunning(false); if ($exception) { throw $exception; } return $this->responses; } /** * Returns if the pipeline should throw exceptions on server errors. * * @return bool */ protected function throwServerExceptions() { return (bool) $this->client->getOptions()->exceptions; } /** * Returns the underlying client instance used by the pipeline object. * * @return ClientInterface */ public function getClient() { return $this->client; } } /** * Command pipeline that writes commands to the servers but discards responses. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class FireAndForget extends Pipeline { /** * {@inheritdoc} */ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { while (!$commands->isEmpty()) { $connection->writeRequest($commands->dequeue()); } $connection->disconnect(); return array(); } } /** * Command pipeline that does not throw exceptions on connection errors, but * returns the exception instances as the rest of the response elements. * * @todo Awful naming! * @author Daniele Alessandri <suppakilla@gmail.com> */ class ConnectionErrorProof extends Pipeline { /** * {@inheritdoc} */ protected function getConnection() { return $this->getClient()->getConnection(); } /** * {@inheritdoc} */ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { if ($connection instanceof NodeConnectionInterface) { return $this->executeSingleNode($connection, $commands); } elseif ($connection instanceof ClusterInterface) { return $this->executeCluster($connection, $commands); } else { $class = get_class($connection); throw new NotSupportedException("The connection class '$class' is not supported."); } } /** * {@inheritdoc} */ protected function executeSingleNode(NodeConnectionInterface $connection, SplQueue $commands) { $responses = array(); $sizeOfPipe = count($commands); foreach ($commands as $command) { try { $connection->writeRequest($command); } catch (CommunicationException $exception) { return array_fill(0, $sizeOfPipe, $exception); } } for ($i = 0; $i < $sizeOfPipe; $i++) { $command = $commands->dequeue(); try { $responses[$i] = $connection->readResponse($command); } catch (CommunicationException $exception) { $add = count($commands) - count($responses); $responses = array_merge($responses, array_fill(0, $add, $exception)); break; } } return $responses; } /** * {@inheritdoc} */ protected function executeCluster(ClusterInterface $connection, SplQueue $commands) { $responses = array(); $sizeOfPipe = count($commands); $exceptions = array(); foreach ($commands as $command) { $cmdConnection = $connection->getConnection($command); if (isset($exceptions[spl_object_hash($cmdConnection)])) { continue; } try { $cmdConnection->writeRequest($command); } catch (CommunicationException $exception) { $exceptions[spl_object_hash($cmdConnection)] = $exception; } } for ($i = 0; $i < $sizeOfPipe; $i++) { $command = $commands->dequeue(); $cmdConnection = $connection->getConnection($command); $connectionHash = spl_object_hash($cmdConnection); if (isset($exceptions[$connectionHash])) { $responses[$i] = $exceptions[$connectionHash]; continue; } try { $responses[$i] = $cmdConnection->readResponse($command); } catch (CommunicationException $exception) { $responses[$i] = $exception; $exceptions[$connectionHash] = $exception; } } return $responses; } } /** * Command pipeline wrapped into a MULTI / EXEC transaction. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Atomic extends Pipeline { /** * {@inheritdoc} */ public function __construct(ClientInterface $client) { if (!$client->getProfile()->supportsCommands(array('multi', 'exec', 'discard'))) { throw new ClientException( "The current profile does not support 'MULTI', 'EXEC' and 'DISCARD'." ); } parent::__construct($client); } /** * {@inheritdoc} */ protected function getConnection() { $connection = $this->getClient()->getConnection(); if (!$connection instanceof NodeConnectionInterface) { $class = __CLASS__; throw new ClientException("The class '$class' does not support aggregate connections."); } return $connection; } /** * {@inheritdoc} */ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { $profile = $this->getClient()->getProfile(); $connection->executeCommand($profile->createCommand('multi')); foreach ($commands as $command) { $connection->writeRequest($command); } foreach ($commands as $command) { $response = $connection->readResponse($command); if ($response instanceof ErrorResponseInterface) { $connection->executeCommand($profile->createCommand('discard')); throw new ServerException($response->getMessage()); } } $executed = $connection->executeCommand($profile->createCommand('exec')); if (!isset($executed)) { // TODO: should be throwing a more appropriate exception. throw new ClientException( 'The underlying transaction has been aborted by the server.' ); } if (count($executed) !== count($commands)) { $expected = count($commands); $received = count($executed); throw new ClientException( "Invalid number of responses [expected $expected, received $received]." ); } $responses = array(); $sizeOfPipe = count($commands); $exceptions = $this->throwServerExceptions(); for ($i = 0; $i < $sizeOfPipe; $i++) { $command = $commands->dequeue(); $response = $executed[$i]; if (!$response instanceof ResponseInterface) { $responses[] = $command->parseResponse($response); } elseif ($response instanceof ErrorResponseInterface && $exceptions) { $this->exception($connection, $response); } else { $responses[] = $response; } unset($executed[$i]); } return $responses; } } /* --------------------------------------------------------------------------- */ namespace Predis\Cluster\Distributor; use Predis\Cluster\Hash\HashGeneratorInterface; use Exception; /** * A distributor implements the logic to automatically distribute keys among * several nodes for client-side sharding. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface DistributorInterface { /** * Adds a node to the distributor with an optional weight. * * @param mixed $node Node object. * @param int $weight Weight for the node. */ public function add($node, $weight = null); /** * Removes a node from the distributor. * * @param mixed $node Node object. */ public function remove($node); /** * Returns the corresponding slot of a node from the distributor using the * computed hash of a key. * * @param mixed $hash * * @return mixed */ public function getSlot($hash); /** * Returns a node from the distributor using its assigned slot ID. * * @param mixed $slot * * @return mixed|null */ public function getBySlot($slot); /** * Returns a node from the distributor using the computed hash of a key. * * @param mixed $hash * * @return mixed */ public function getByHash($hash); /** * Returns a node from the distributor mapping to the specified value. * * @param string $value * * @return mixed */ public function get($value); /** * Returns the underlying hash generator instance. * * @return HashGeneratorInterface */ public function getHashGenerator(); } /** * This class implements an hashring-based distributor that uses the same * algorithm of memcache to distribute keys in a cluster using client-side * sharding. * * @author Daniele Alessandri <suppakilla@gmail.com> * @author Lorenzo Castelli <lcastelli@gmail.com> */ class HashRing implements DistributorInterface, HashGeneratorInterface { const DEFAULT_REPLICAS = 128; const DEFAULT_WEIGHT = 100; private $ring; private $ringKeys; private $ringKeysCount; private $replicas; private $nodeHashCallback; private $nodes = array(); /** * @param int $replicas Number of replicas in the ring. * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes. */ public function __construct($replicas = self::DEFAULT_REPLICAS, $nodeHashCallback = null) { $this->replicas = $replicas; $this->nodeHashCallback = $nodeHashCallback; } /** * Adds a node to the ring with an optional weight. * * @param mixed $node Node object. * @param int $weight Weight for the node. */ public function add($node, $weight = null) { // In case of collisions in the hashes of the nodes, the node added // last wins, thus the order in which nodes are added is significant. $this->nodes[] = array( 'object' => $node, 'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT ); $this->reset(); } /** * {@inheritdoc} */ public function remove($node) { // A node is removed by resetting the ring so that it's recreated from // scratch, in order to reassign possible hashes with collisions to the // right node according to the order in which they were added in the // first place. for ($i = 0; $i < count($this->nodes); ++$i) { if ($this->nodes[$i]['object'] === $node) { array_splice($this->nodes, $i, 1); $this->reset(); break; } } } /** * Resets the distributor. */ private function reset() { unset( $this->ring, $this->ringKeys, $this->ringKeysCount ); } /** * Returns the initialization status of the distributor. * * @return bool */ private function isInitialized() { return isset($this->ringKeys); } /** * Calculates the total weight of all the nodes in the distributor. * * @return int */ private function computeTotalWeight() { $totalWeight = 0; foreach ($this->nodes as $node) { $totalWeight += $node['weight']; } return $totalWeight; } /** * Initializes the distributor. */ private function initialize() { if ($this->isInitialized()) { return; } if (!$this->nodes) { throw new EmptyRingException('Cannot initialize an empty hashring.'); } $this->ring = array(); $totalWeight = $this->computeTotalWeight(); $nodesCount = count($this->nodes); foreach ($this->nodes as $node) { $weightRatio = $node['weight'] / $totalWeight; $this->addNodeToRing($this->ring, $node, $nodesCount, $this->replicas, $weightRatio); } ksort($this->ring, SORT_NUMERIC); $this->ringKeys = array_keys($this->ring); $this->ringKeysCount = count($this->ringKeys); } /** * Implements the logic needed to add a node to the hashring. * * @param array $ring Source hashring. * @param mixed $node Node object to be added. * @param int $totalNodes Total number of nodes. * @param int $replicas Number of replicas in the ring. * @param float $weightRatio Weight ratio for the node. */ protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio) { $nodeObject = $node['object']; $nodeHash = $this->getNodeHash($nodeObject); $replicas = (int) round($weightRatio * $totalNodes * $replicas); for ($i = 0; $i < $replicas; $i++) { $key = crc32("$nodeHash:$i"); $ring[$key] = $nodeObject; } } /** * {@inheritdoc} */ protected function getNodeHash($nodeObject) { if (!isset($this->nodeHashCallback)) { return (string) $nodeObject; } return call_user_func($this->nodeHashCallback, $nodeObject); } /** * {@inheritdoc} */ public function hash($value) { return crc32($value); } /** * {@inheritdoc} */ public function getByHash($hash) { return $this->ring[$this->getSlot($hash)]; } /** * {@inheritdoc} */ public function getBySlot($slot) { $this->initialize(); if (isset($this->ring[$slot])) { return $this->ring[$slot]; } } /** * {@inheritdoc} */ public function getSlot($hash) { $this->initialize(); $ringKeys = $this->ringKeys; $upper = $this->ringKeysCount - 1; $lower = 0; while ($lower <= $upper) { $index = ($lower + $upper) >> 1; $item = $ringKeys[$index]; if ($item > $hash) { $upper = $index - 1; } elseif ($item < $hash) { $lower = $index + 1; } else { return $item; } } return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)]; } /** * {@inheritdoc} */ public function get($value) { $hash = $this->hash($value); $node = $this->getByHash($hash); return $node; } /** * Implements a strategy to deal with wrap-around errors during binary searches. * * @param int $upper * @param int $lower * @param int $ringKeysCount * * @return int */ protected function wrapAroundStrategy($upper, $lower, $ringKeysCount) { // Binary search for the last item in ringkeys with a value less or // equal to the key. If no such item exists, return the last item. return $upper >= 0 ? $upper : $ringKeysCount - 1; } /** * {@inheritdoc} */ public function getHashGenerator() { return $this; } } /** * This class implements an hashring-based distributor that uses the same * algorithm of libketama to distribute keys in a cluster using client-side * sharding. * * @author Daniele Alessandri <suppakilla@gmail.com> * @author Lorenzo Castelli <lcastelli@gmail.com> */ class KetamaRing extends HashRing { const DEFAULT_REPLICAS = 160; /** * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes. */ public function __construct($nodeHashCallback = null) { parent::__construct($this::DEFAULT_REPLICAS, $nodeHashCallback); } /** * {@inheritdoc} */ protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio) { $nodeObject = $node['object']; $nodeHash = $this->getNodeHash($nodeObject); $replicas = (int) floor($weightRatio * $totalNodes * ($replicas / 4)); for ($i = 0; $i < $replicas; $i++) { $unpackedDigest = unpack('V4', md5("$nodeHash-$i", true)); foreach ($unpackedDigest as $key) { $ring[$key] = $nodeObject; } } } /** * {@inheritdoc} */ public function hash($value) { $hash = unpack('V', md5($value, true)); return $hash[1]; } /** * {@inheritdoc} */ protected function wrapAroundStrategy($upper, $lower, $ringKeysCount) { // Binary search for the first item in ringkeys with a value greater // or equal to the key. If no such item exists, return the first item. return $lower < $ringKeysCount ? $lower : 0; } } /** * Exception class that identifies empty rings. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class EmptyRingException extends Exception { } /* --------------------------------------------------------------------------- */ namespace Predis\Response\Iterator; use Predis\Connection\NodeConnectionInterface; use Iterator; use Countable; use Predis\Response\ResponseInterface; use OuterIterator; use InvalidArgumentException; use UnexpectedValueException; /** * Iterator that abstracts the access to multibulk responses allowing them to be * consumed in a streamable fashion without keeping the whole payload in memory. * * This iterator does not support rewinding which means that the iteration, once * consumed, cannot be restarted. * * Always make sure that the whole iteration is consumed (or dropped) to prevent * protocol desynchronization issues. * * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class MultiBulkIterator implements Iterator, Countable, ResponseInterface { protected $current; protected $position; protected $size; /** * {@inheritdoc} */ public function rewind() { // NOOP } /** * {@inheritdoc} */ public function current() { return $this->current; } /** * {@inheritdoc} */ public function key() { return $this->position; } /** * {@inheritdoc} */ public function next() { if (++$this->position < $this->size) { $this->current = $this->getValue(); } } /** * {@inheritdoc} */ public function valid() { return $this->position < $this->size; } /** * Returns the number of items comprising the whole multibulk response. * * This method should be used instead of iterator_count() to get the size of * the current multibulk response since the former consumes the iteration to * count the number of elements, but our iterators do not support rewinding. * * @return int */ public function count() { return $this->size; } /** * Returns the current position of the iterator. * * @return int */ public function getPosition() { return $this->position; } /** * {@inheritdoc} */ abstract protected function getValue(); } /** * Streamable multibulk response. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class MultiBulk extends MultiBulkIterator { private $connection; /** * @param NodeConnectionInterface $connection Connection to Redis. * @param int $size Number of elements of the multibulk response. */ public function __construct(NodeConnectionInterface $connection, $size) { $this->connection = $connection; $this->size = $size; $this->position = 0; $this->current = $size > 0 ? $this->getValue() : null; } /** * Handles the synchronization of the client with the Redis protocol when * the garbage collector kicks in (e.g. when the iterator goes out of the * scope of a foreach or it is unset). */ public function __destruct() { $this->drop(true); } /** * Drop queued elements that have not been read from the connection either * by consuming the rest of the multibulk response or quickly by closing the * underlying connection. * * @param bool $disconnect Consume the iterator or drop the connection. */ public function drop($disconnect = false) { if ($disconnect) { if ($this->valid()) { $this->position = $this->size; $this->connection->disconnect(); } } else { while ($this->valid()) { $this->next(); } } } /** * Reads the next item of the multibulk response from the connection. * * @return mixed */ protected function getValue() { return $this->connection->read(); } } /** * Outer iterator consuming streamable multibulk responses by yielding tuples of * keys and values. * * This wrapper is useful for responses to commands such as `HGETALL` that can * be iterater as $key => $value pairs. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class MultiBulkTuple extends MultiBulk implements OuterIterator { private $iterator; /** * @param MultiBulk $iterator Inner multibulk response iterator. */ public function __construct(MultiBulk $iterator) { $this->checkPreconditions($iterator); $this->size = count($iterator) / 2; $this->iterator = $iterator; $this->position = $iterator->getPosition(); $this->current = $this->size > 0 ? $this->getValue() : null; } /** * Checks for valid preconditions. * * @param MultiBulk $iterator Inner multibulk response iterator. * * @throws \InvalidArgumentException * @throws \UnexpectedValueException */ protected function checkPreconditions(MultiBulk $iterator) { if ($iterator->getPosition() !== 0) { throw new InvalidArgumentException( 'Cannot initialize a tuple iterator using an already initiated iterator.' ); } if (($size = count($iterator)) % 2 !== 0) { throw new UnexpectedValueException("Invalid response size for a tuple iterator."); } } /** * {@inheritdoc} */ public function getInnerIterator() { return $this->iterator; } /** * {@inheritdoc} */ public function __destruct() { $this->iterator->drop(true); } /** * {@inheritdoc} */ protected function getValue() { $k = $this->iterator->current(); $this->iterator->next(); $v = $this->iterator->current(); $this->iterator->next(); return array($k, $v); } } /* --------------------------------------------------------------------------- */ namespace Predis\Cluster\Hash; /** * An hash generator implements the logic used to calculate the hash of a key to * distribute operations among Redis nodes. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface HashGeneratorInterface { /** * Generates an hash from a string to be used for distribution. * * @param string $value String value. * * @return int */ public function hash($value); } /** * Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class CRC16 implements HashGeneratorInterface { private static $CCITT_16 = array( 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0, ); /** * {@inheritdoc} */ public function hash($value) { // CRC-CCITT-16 algorithm $crc = 0; $CCITT_16 = self::$CCITT_16; $strlen = strlen($value); for ($i = 0; $i < $strlen; $i++) { $crc = (($crc << 8) ^ $CCITT_16[($crc >> 8) ^ ord($value[$i])]) & 0xFFFF; } return $crc; } } /* --------------------------------------------------------------------------- */ namespace Predis\Command\Processor; use InvalidArgumentException; use Predis\Command\CommandInterface; use Predis\Command\PrefixableCommandInterface; use ArrayAccess; use ArrayIterator; /** * A command processor processes Redis commands before they are sent to Redis. * * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ProcessorInterface { /** * Processes the given Redis command. * * @param CommandInterface $command Command instance. */ public function process(CommandInterface $command); } /** * Default implementation of a command processors chain. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ProcessorChain implements ArrayAccess, ProcessorInterface { private $processors = array(); /** * @param array $processors List of instances of ProcessorInterface. */ public function __construct($processors = array()) { foreach ($processors as $processor) { $this->add($processor); } } /** * {@inheritdoc} */ public function add(ProcessorInterface $processor) { $this->processors[] = $processor; } /** * {@inheritdoc} */ public function remove(ProcessorInterface $processor) { if (false !== $index = array_search($processor, $this->processors, true)) { unset($this[$index]); } } /** * {@inheritdoc} */ public function process(CommandInterface $command) { for ($i = 0; $i < $count = count($this->processors); $i++) { $this->processors[$i]->process($command); } } /** * {@inheritdoc} */ public function getProcessors() { return $this->processors; } /** * Returns an iterator over the list of command processor in the chain. * * @return ArrayIterator */ public function getIterator() { return new ArrayIterator($this->processors); } /** * Returns the number of command processors in the chain. * * @return int */ public function count() { return count($this->processors); } /** * {@inheritdoc} */ public function offsetExists($index) { return isset($this->processors[$index]); } /** * {@inheritdoc} */ public function offsetGet($index) { return $this->processors[$index]; } /** * {@inheritdoc} */ public function offsetSet($index, $processor) { if (!$processor instanceof ProcessorInterface) { throw new InvalidArgumentException( "A processor chain accepts only instances of ". "'Predis\Command\Processor\ProcessorInterface'." ); } $this->processors[$index] = $processor; } /** * {@inheritdoc} */ public function offsetUnset($index) { unset($this->processors[$index]); $this->processors = array_values($this->processors); } } /** * Command processor capable of prefixing keys stored in the arguments of Redis * commands supported. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class KeyPrefixProcessor implements ProcessorInterface { private $prefix; private $commands; /** * @param string $prefix Prefix for the keys. */ public function __construct($prefix) { $this->prefix = $prefix; $this->commands = array( /* ---------------- Redis 1.2 ---------------- */ 'EXISTS' => 'self::first', 'DEL' => 'self::all', 'TYPE' => 'self::first', 'KEYS' => 'self::first', 'RENAME' => 'self::all', 'RENAMENX' => 'self::all', 'EXPIRE' => 'self::first', 'EXPIREAT' => 'self::first', 'TTL' => 'self::first', 'MOVE' => 'self::first', 'SORT' => 'self::sort', 'DUMP' => 'self::first', 'RESTORE' => 'self::first', 'SET' => 'self::first', 'SETNX' => 'self::first', 'MSET' => 'self::interleaved', 'MSETNX' => 'self::interleaved', 'GET' => 'self::first', 'MGET' => 'self::all', 'GETSET' => 'self::first', 'INCR' => 'self::first', 'INCRBY' => 'self::first', 'DECR' => 'self::first', 'DECRBY' => 'self::first', 'RPUSH' => 'self::first', 'LPUSH' => 'self::first', 'LLEN' => 'self::first', 'LRANGE' => 'self::first', 'LTRIM' => 'self::first', 'LINDEX' => 'self::first', 'LSET' => 'self::first', 'LREM' => 'self::first', 'LPOP' => 'self::first', 'RPOP' => 'self::first', 'RPOPLPUSH' => 'self::all', 'SADD' => 'self::first', 'SREM' => 'self::first', 'SPOP' => 'self::first', 'SMOVE' => 'self::skipLast', 'SCARD' => 'self::first', 'SISMEMBER' => 'self::first', 'SINTER' => 'self::all', 'SINTERSTORE' => 'self::all', 'SUNION' => 'self::all', 'SUNIONSTORE' => 'self::all', 'SDIFF' => 'self::all', 'SDIFFSTORE' => 'self::all', 'SMEMBERS' => 'self::first', 'SRANDMEMBER' => 'self::first', 'ZADD' => 'self::first', 'ZINCRBY' => 'self::first', 'ZREM' => 'self::first', 'ZRANGE' => 'self::first', 'ZREVRANGE' => 'self::first', 'ZRANGEBYSCORE' => 'self::first', 'ZCARD' => 'self::first', 'ZSCORE' => 'self::first', 'ZREMRANGEBYSCORE' => 'self::first', /* ---------------- Redis 2.0 ---------------- */ 'SETEX' => 'self::first', 'APPEND' => 'self::first', 'SUBSTR' => 'self::first', 'BLPOP' => 'self::skipLast', 'BRPOP' => 'self::skipLast', 'ZUNIONSTORE' => 'self::zsetStore', 'ZINTERSTORE' => 'self::zsetStore', 'ZCOUNT' => 'self::first', 'ZRANK' => 'self::first', 'ZREVRANK' => 'self::first', 'ZREMRANGEBYRANK' => 'self::first', 'HSET' => 'self::first', 'HSETNX' => 'self::first', 'HMSET' => 'self::first', 'HINCRBY' => 'self::first', 'HGET' => 'self::first', 'HMGET' => 'self::first', 'HDEL' => 'self::first', 'HEXISTS' => 'self::first', 'HLEN' => 'self::first', 'HKEYS' => 'self::first', 'HVALS' => 'self::first', 'HGETALL' => 'self::first', 'SUBSCRIBE' => 'self::all', 'UNSUBSCRIBE' => 'self::all', 'PSUBSCRIBE' => 'self::all', 'PUNSUBSCRIBE' => 'self::all', 'PUBLISH' => 'self::first', /* ---------------- Redis 2.2 ---------------- */ 'PERSIST' => 'self::first', 'STRLEN' => 'self::first', 'SETRANGE' => 'self::first', 'GETRANGE' => 'self::first', 'SETBIT' => 'self::first', 'GETBIT' => 'self::first', 'RPUSHX' => 'self::first', 'LPUSHX' => 'self::first', 'LINSERT' => 'self::first', 'BRPOPLPUSH' => 'self::skipLast', 'ZREVRANGEBYSCORE' => 'self::first', 'WATCH' => 'self::all', /* ---------------- Redis 2.6 ---------------- */ 'PTTL' => 'self::first', 'PEXPIRE' => 'self::first', 'PEXPIREAT' => 'self::first', 'PSETEX' => 'self::first', 'INCRBYFLOAT' => 'self::first', 'BITOP' => 'self::skipFirst', 'BITCOUNT' => 'self::first', 'HINCRBYFLOAT' => 'self::first', 'EVAL' => 'self::evalKeys', 'EVALSHA' => 'self::evalKeys', /* ---------------- Redis 2.8 ---------------- */ 'SSCAN' => 'self::first', 'ZSCAN' => 'self::first', 'HSCAN' => 'self::first', 'PFADD' => 'self::first', 'PFCOUNT' => 'self::all', 'PFMERGE' => 'self::all', 'ZLEXCOUNT' => 'self::first', 'ZRANGEBYLEX' => 'self::first', 'ZREMRANGEBYLEX' => 'self::first', ); } /** * Sets a prefix that is applied to all the keys. * * @param string $prefix Prefix for the keys. */ public function setPrefix($prefix) { $this->prefix = $prefix; } /** * Gets the current prefix. * * @return string */ public function getPrefix() { return $this->prefix; } /** * {@inheritdoc} */ public function process(CommandInterface $command) { if ($command instanceof PrefixableCommandInterface) { $command->prefixKeys($this->prefix); } elseif (isset($this->commands[$commandID = strtoupper($command->getId())])) { call_user_func($this->commands[$commandID], $command, $this->prefix); } } /** * Sets an handler for the specified command ID. * * The callback signature must have 2 parameters of the following types: * * - Predis\Command\CommandInterface (command instance) * - String (prefix) * * When the callback argument is omitted or NULL, the previously * associated handler for the specified command ID is removed. * * @param string $commandID The ID of the command to be handled. * @param mixed $callback A valid callable object or NULL. * * @throws \InvalidArgumentException */ public function setCommandHandler($commandID, $callback = null) { $commandID = strtoupper($commandID); if (!isset($callback)) { unset($this->commands[$commandID]); return; } if (!is_callable($callback)) { throw new InvalidArgumentException( "Callback must be a valid callable object or NULL" ); } $this->commands[$commandID] = $callback; } /** * {@inheritdoc} */ public function __toString() { return $this->getPrefix(); } /** * Applies the specified prefix only the first argument. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function first(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $arguments[0] = "$prefix{$arguments[0]}"; $command->setRawArguments($arguments); } } /** * Applies the specified prefix to all the arguments. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function all(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { foreach ($arguments as &$key) { $key = "$prefix$key"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix only to even arguments in the list. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function interleaved(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $length = count($arguments); for ($i = 0; $i < $length; $i += 2) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to all the arguments but the first one. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function skipFirst(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $length = count($arguments); for ($i = 1; $i < $length; $i++) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to all the arguments but the last one. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function skipLast(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $length = count($arguments); for ($i = 0; $i < $length - 1; $i++) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to the keys of a SORT command. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function sort(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $arguments[0] = "$prefix{$arguments[0]}"; if (($count = count($arguments)) > 1) { for ($i = 1; $i < $count; $i++) { switch ($arguments[$i]) { case 'BY': case 'STORE': $arguments[$i] = "$prefix{$arguments[++$i]}"; break; case 'GET': $value = $arguments[++$i]; if ($value !== '#') { $arguments[$i] = "$prefix$value"; } break; case 'LIMIT'; $i += 2; break; } } } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to the keys of an EVAL-based command. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function evalKeys(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { for ($i = 2; $i < $arguments[1] + 2; $i++) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to the keys of Z[INTERSECTION|UNION]STORE. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function zsetStore(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $arguments[0] = "$prefix{$arguments[0]}"; $length = ((int) $arguments[1]) + 2; for ($i = 2; $i < $length; $i++) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } } /* --------------------------------------------------------------------------- */ namespace Predis\Protocol\Text; use Predis\Command\CommandInterface; use Predis\Connection\CompositeConnectionInterface; use Predis\Protocol\ProtocolProcessorInterface; use Predis\Protocol\RequestSerializerInterface; use Predis\Protocol\ResponseReaderInterface; use Predis\CommunicationException; use Predis\Protocol\ProtocolException; use Predis\Response\Status as StatusResponse; use Predis\Response\Error as ErrorResponse; use Predis\Response\Iterator\MultiBulk as MultiBulkIterator; /** * Response reader for the standard Redis wire protocol. * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class ResponseReader implements ResponseReaderInterface { protected $handlers; /** * */ public function __construct() { $this->handlers = $this->getDefaultHandlers(); } /** * Returns the default handlers for the supported type of responses. * * @return array */ protected function getDefaultHandlers() { return array( '+' => new Handler\StatusResponse(), '-' => new Handler\ErrorResponse(), ':' => new Handler\IntegerResponse(), '$' => new Handler\BulkResponse(), '*' => new Handler\MultiBulkResponse(), ); } /** * Sets the handler for the specified prefix identifying the response type. * * @param string $prefix Identifier of the type of response. * @param Handler\ResponseHandlerInterface $handler Response handler. */ public function setHandler($prefix, Handler\ResponseHandlerInterface $handler) { $this->handlers[$prefix] = $handler; } /** * Returns the response handler associated to a certain type of response. * * @param string $prefix Identifier of the type of response. * * @return Handler\ResponseHandlerInterface */ public function getHandler($prefix) { if (isset($this->handlers[$prefix])) { return $this->handlers[$prefix]; } return null; } /** * {@inheritdoc} */ public function read(CompositeConnectionInterface $connection) { $header = $connection->readLine(); if ($header === '') { $this->onProtocolError($connection, 'Unexpected empty reponse header.'); } $prefix = $header[0]; if (!isset($this->handlers[$prefix])) { $this->onProtocolError($connection, "Unknown response prefix: '$prefix'."); } $payload = $this->handlers[$prefix]->handle($connection, substr($header, 1)); return $payload; } /** * Handles protocol errors generated while reading responses from a * connection. * * @param CompositeConnectionInterface $connection Redis connection that generated the error. * @param string $message Error message. */ protected function onProtocolError(CompositeConnectionInterface $connection, $message) { CommunicationException::handle( new ProtocolException($connection, $message) ); } } /** * Request serializer for the standard Redis wire protocol. * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class RequestSerializer implements RequestSerializerInterface { /** * {@inheritdoc} */ public function serialize(CommandInterface $command) { $commandID = $command->getId(); $arguments = $command->getArguments(); $cmdlen = strlen($commandID); $reqlen = count($arguments) + 1; $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n"; for ($i = 0, $reqlen--; $i < $reqlen; $i++) { $argument = $arguments[$i]; $arglen = strlen($argument); $buffer .= "\${$arglen}\r\n{$argument}\r\n"; } return $buffer; } } /** * Protocol processor for the standard Redis wire protocol. * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class ProtocolProcessor implements ProtocolProcessorInterface { protected $mbiterable; protected $serializer; /** * */ public function __construct() { $this->mbiterable = false; $this->serializer = new RequestSerializer(); } /** * {@inheritdoc} */ public function write(CompositeConnectionInterface $connection, CommandInterface $command) { $request = $this->serializer->serialize($command); $connection->writeBuffer($request); } /** * {@inheritdoc} */ public function read(CompositeConnectionInterface $connection) { $chunk = $connection->readLine(); $prefix = $chunk[0]; $payload = substr($chunk, 1); switch ($prefix) { case '+': return new StatusResponse($payload); case '$': $size = (int) $payload; if ($size === -1) { return null; } return substr($connection->readBuffer($size + 2), 0, -2); case '*': $count = (int) $payload; if ($count === -1) { return null; } if ($this->mbiterable) { return new MultiBulkIterator($connection, $count); } $multibulk = array(); for ($i = 0; $i < $count; $i++) { $multibulk[$i] = $this->read($connection); } return $multibulk; case ':': return (int) $payload; case '-': return new ErrorResponse($payload); default: CommunicationException::handle(new ProtocolException( $connection, "Unknown response prefix: '$prefix'." )); return; } } /** * Enables or disables returning multibulk responses as specialized PHP * iterators used to stream bulk elements of a multibulk response instead * returning a plain array. * * Streamable multibulk responses are not globally supported by the * abstractions built-in into Predis, such as transactions or pipelines. * Use them with care! * * @param bool $value Enable or disable streamable multibulk responses. */ public function useIterableMultibulk($value) { $this->mbiterable = (bool) $value; } } /** * Composite protocol processor for the standard Redis wire protocol using * pluggable handlers to serialize requests and deserialize responses. * * @link http://redis.io/topics/protocol * @author Daniele Alessandri <suppakilla@gmail.com> */ class CompositeProtocolProcessor implements ProtocolProcessorInterface { /* * @var RequestSerializerInterface */ protected $serializer; /* * @var ResponseReaderInterface */ protected $reader; /** * @param RequestSerializerInterface $serializer Request serializer. * @param ResponseReaderInterface $reader Response reader. */ public function __construct( RequestSerializerInterface $serializer = null, ResponseReaderInterface $reader = null ) { $this->setRequestSerializer($serializer ?: new RequestSerializer()); $this->setResponseReader($reader ?: new ResponseReader()); } /** * {@inheritdoc} */ public function write(CompositeConnectionInterface $connection, CommandInterface $command) { $connection->writeBuffer($this->serializer->serialize($command)); } /** * {@inheritdoc} */ public function read(CompositeConnectionInterface $connection) { return $this->reader->read($connection); } /** * Sets the request serializer used by the protocol processor. * * @param RequestSerializerInterface $serializer Request serializer. */ public function setRequestSerializer(RequestSerializerInterface $serializer) { $this->serializer = $serializer; } /** * Returns the request serializer used by the protocol processor. * * @return RequestSerializerInterface */ public function getRequestSerializer() { return $this->serializer; } /** * Sets the response reader used by the protocol processor. * * @param ResponseReaderInterface $reader Response reader. */ public function setResponseReader(ResponseReaderInterface $reader) { $this->reader = $reader; } /** * Returns the Response reader used by the protocol processor. * * @return ResponseReaderInterface */ public function getResponseReader() { return $this->reader; } } /* --------------------------------------------------------------------------- */ namespace Predis\PubSub; use Iterator; use Predis\ClientException; use Predis\ClientInterface; use Predis\Command\Command; use Predis\NotSupportedException; use Predis\Connection\AggregateConnectionInterface; use InvalidArgumentException; /** * Base implementation of a PUB/SUB consumer abstraction based on PHP iterators. * * @author Daniele Alessandri <suppakilla@gmail.com> */ abstract class AbstractConsumer implements Iterator { const SUBSCRIBE = 'subscribe'; const UNSUBSCRIBE = 'unsubscribe'; const PSUBSCRIBE = 'psubscribe'; const PUNSUBSCRIBE = 'punsubscribe'; const MESSAGE = 'message'; const PMESSAGE = 'pmessage'; const PONG = 'pong'; const STATUS_VALID = 1; // 0b0001 const STATUS_SUBSCRIBED = 2; // 0b0010 const STATUS_PSUBSCRIBED = 4; // 0b0100 private $position = null; private $statusFlags = self::STATUS_VALID; /** * Automatically stops the consumer when the garbage collector kicks in. */ public function __destruct() { $this->stop(true); } /** * Checks if the specified flag is valid based on the state of the consumer. * * @param int $value Flag. * * @return bool */ protected function isFlagSet($value) { return ($this->statusFlags & $value) === $value; } /** * Subscribes to the specified channels. * * @param mixed $channel,... One or more channel names. */ public function subscribe($channel /*, ... */) { $this->writeRequest(self::SUBSCRIBE, func_get_args()); $this->statusFlags |= self::STATUS_SUBSCRIBED; } /** * Unsubscribes from the specified channels. * * @param string ... One or more channel names. */ public function unsubscribe(/* ... */) { $this->writeRequest(self::UNSUBSCRIBE, func_get_args()); } /** * Subscribes to the specified channels using a pattern. * * @param mixed $pattern,... One or more channel name patterns. */ public function psubscribe($pattern /* ... */) { $this->writeRequest(self::PSUBSCRIBE, func_get_args()); $this->statusFlags |= self::STATUS_PSUBSCRIBED; } /** * Unsubscribes from the specified channels using a pattern. * * @param string ... One or more channel name patterns. */ public function punsubscribe(/* ... */) { $this->writeRequest(self::PUNSUBSCRIBE, func_get_args()); } /** * PING the server with an optional payload that will be echoed as a * PONG message in the pub/sub loop. * * @param string $payload Optional PING payload. */ public function ping($payload = null) { $this->writeRequest('PING', array($payload)); } /** * Closes the context by unsubscribing from all the subscribed channels. The * context can be forcefully closed by dropping the underlying connection. * * @param bool $drop Indicates if the context should be closed by dropping the connection. * * @return bool Returns false when there are no pending messages. */ public function stop($drop = false) { if (!$this->valid()) { return false; } if ($drop) { $this->invalidate(); $this->disconnect(); } else { if ($this->isFlagSet(self::STATUS_SUBSCRIBED)) { $this->unsubscribe(); } if ($this->isFlagSet(self::STATUS_PSUBSCRIBED)) { $this->punsubscribe(); } } return !$drop; } /** * Closes the underlying connection when forcing a disconnection. */ abstract protected function disconnect(); /** * Writes a Redis command on the underlying connection. * * @param string $method Command ID. * @param array $arguments Arguments for the command. */ abstract protected function writeRequest($method, $arguments); /** * {@inheritdoc} */ public function rewind() { // NOOP } /** * Returns the last message payload retrieved from the server and generated * by one of the active subscriptions. * * @return array */ public function current() { return $this->getValue(); } /** * {@inheritdoc} */ public function key() { return $this->position; } /** * {@inheritdoc} */ public function next() { if ($this->valid()) { $this->position++; } return $this->position; } /** * Checks if the the consumer is still in a valid state to continue. * * @return bool */ public function valid() { $isValid = $this->isFlagSet(self::STATUS_VALID); $subscriptionFlags = self::STATUS_SUBSCRIBED | self::STATUS_PSUBSCRIBED; $hasSubscriptions = ($this->statusFlags & $subscriptionFlags) > 0; return $isValid && $hasSubscriptions; } /** * Resets the state of the consumer. */ protected function invalidate() { $this->statusFlags = 0; // 0b0000; } /** * Waits for a new message from the server generated by one of the active * subscriptions and returns it when available. * * @return array */ abstract protected function getValue(); } /** * Method-dispatcher loop built around the client-side abstraction of a Redis * PUB / SUB context. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class DispatcherLoop { private $pubsub; protected $callbacks; protected $defaultCallback; protected $subscriptionCallback; /** * @param Consumer $pubsub PubSub consumer instance used by the loop. */ public function __construct(Consumer $pubsub) { $this->callbacks = array(); $this->pubsub = $pubsub; } /** * Checks if the passed argument is a valid callback. * * @param mixed $callable A callback. * * @throws \InvalidArgumentException */ protected function assertCallback($callable) { if (!is_callable($callable)) { throw new InvalidArgumentException('The given argument must be a callable object.'); } } /** * Returns the underlying PUB / SUB context. * * @return Consumer */ public function getPubSubConsumer() { return $this->pubsub; } /** * Sets a callback that gets invoked upon new subscriptions. * * @param mixed $callable A callback. */ public function subscriptionCallback($callable = null) { if (isset($callable)) { $this->assertCallback($callable); } $this->subscriptionCallback = $callable; } /** * Sets a callback that gets invoked when a message is received on a * channel that does not have an associated callback. * * @param mixed $callable A callback. */ public function defaultCallback($callable = null) { if (isset($callable)) { $this->assertCallback($callable); } $this->subscriptionCallback = $callable; } /** * Binds a callback to a channel. * * @param string $channel Channel name. * @param Callable $callback A callback. */ public function attachCallback($channel, $callback) { $callbackName = $this->getPrefixKeys() . $channel; $this->assertCallback($callback); $this->callbacks[$callbackName] = $callback; $this->pubsub->subscribe($channel); } /** * Stops listening to a channel and removes the associated callback. * * @param string $channel Redis channel. */ public function detachCallback($channel) { $callbackName = $this->getPrefixKeys() . $channel; if (isset($this->callbacks[$callbackName])) { unset($this->callbacks[$callbackName]); $this->pubsub->unsubscribe($channel); } } /** * Starts the dispatcher loop. */ public function run() { foreach ($this->pubsub as $message) { $kind = $message->kind; if ($kind !== Consumer::MESSAGE && $kind !== Consumer::PMESSAGE) { if (isset($this->subscriptionCallback)) { $callback = $this->subscriptionCallback; call_user_func($callback, $message); } continue; } if (isset($this->callbacks[$message->channel])) { $callback = $this->callbacks[$message->channel]; call_user_func($callback, $message->payload); } elseif (isset($this->defaultCallback)) { $callback = $this->defaultCallback; call_user_func($callback, $message); } } } /** * Terminates the dispatcher loop. */ public function stop() { $this->pubsub->stop(); } /** * Return the prefix used for keys * * @return string */ protected function getPrefixKeys() { $options = $this->pubsub->getClient()->getOptions(); if (isset($options->prefix)) { return $options->prefix->getPrefix(); } return ''; } } /** * PUB/SUB consumer abstraction. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Consumer extends AbstractConsumer { private $client; private $options; /** * @param ClientInterface $client Client instance used by the consumer. * @param array $options Options for the consumer initialization. */ public function __construct(ClientInterface $client, array $options = null) { $this->checkCapabilities($client); $this->options = $options ?: array(); $this->client = $client; $this->genericSubscribeInit('subscribe'); $this->genericSubscribeInit('psubscribe'); } /** * Returns the underlying client instance used by the pub/sub iterator. * * @return ClientInterface */ public function getClient() { return $this->client; } /** * Checks if the client instance satisfies the required conditions needed to * initialize a PUB/SUB consumer. * * @param ClientInterface $client Client instance used by the consumer. * * @throws NotSupportedException */ private function checkCapabilities(ClientInterface $client) { if ($client->getConnection() instanceof AggregateConnectionInterface) { throw new NotSupportedException( 'Cannot initialize a PUB/SUB consumer over aggregate connections.' ); } $commands = array('publish', 'subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe'); if ($client->getProfile()->supportsCommands($commands) === false) { throw new NotSupportedException( 'The current profile does not support PUB/SUB related commands.' ); } } /** * This method shares the logic to handle both SUBSCRIBE and PSUBSCRIBE. * * @param string $subscribeAction Type of subscription. */ private function genericSubscribeInit($subscribeAction) { if (isset($this->options[$subscribeAction])) { $this->$subscribeAction($this->options[$subscribeAction]); } } /** * {@inheritdoc} */ protected function writeRequest($method, $arguments) { $this->client->getConnection()->writeRequest( $this->client->createCommand($method, Command::normalizeArguments($arguments) ) ); } /** * {@inheritdoc} */ protected function disconnect() { $this->client->disconnect(); } /** * {@inheritdoc} */ protected function getValue() { $response = $this->client->getConnection()->read(); switch ($response[0]) { case self::SUBSCRIBE: case self::UNSUBSCRIBE: case self::PSUBSCRIBE: case self::PUNSUBSCRIBE: if ($response[2] === 0) { $this->invalidate(); } // The missing break here is intentional as we must process // subscriptions and unsubscriptions as standard messages. // no break case self::MESSAGE: return (object) array( 'kind' => $response[0], 'channel' => $response[1], 'payload' => $response[2], ); case self::PMESSAGE: return (object) array( 'kind' => $response[0], 'pattern' => $response[1], 'channel' => $response[2], 'payload' => $response[3], ); case self::PONG: return (object) array( 'kind' => $response[0], 'payload' => $response[1], ); default: throw new ClientException( "Unknown message type '{$response[0]}' received in the PUB/SUB context." ); } } } /* --------------------------------------------------------------------------- */ namespace Predis\Transaction; use Predis\PredisException; use Exception; use InvalidArgumentException; use SplQueue; use Predis\ClientContextInterface; use Predis\ClientException; use Predis\ClientInterface; use Predis\CommunicationException; use Predis\NotSupportedException; use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\ServerException; use Predis\Response\Status as StatusResponse; use Predis\Command\CommandInterface; use Predis\Connection\AggregateConnectionInterface; use Predis\Protocol\ProtocolException; /** * Utility class used to track the state of a MULTI / EXEC transaction. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class MultiExecState { const INITIALIZED = 1; // 0b00001 const INSIDEBLOCK = 2; // 0b00010 const DISCARDED = 4; // 0b00100 const CAS = 8; // 0b01000 const WATCH = 16; // 0b10000 private $flags; /** * */ public function __construct() { $this->flags = 0; } /** * Sets the internal state flags. * * @param int $flags Set of flags */ public function set($flags) { $this->flags = $flags; } /** * Gets the internal state flags. * * @return int */ public function get() { return $this->flags; } /** * Sets one or more flags. * * @param int $flags Set of flags */ public function flag($flags) { $this->flags |= $flags; } /** * Resets one or more flags. * * @param int $flags Set of flags */ public function unflag($flags) { $this->flags &= ~$flags; } /** * Returns if the specified flag or set of flags is set. * * @param int $flags Flag * * @return bool */ public function check($flags) { return ($this->flags & $flags) === $flags; } /** * Resets the state of a transaction. */ public function reset() { $this->flags = 0; } /** * Returns the state of the RESET flag. * * @return bool */ public function isReset() { return $this->flags === 0; } /** * Returns the state of the INITIALIZED flag. * * @return bool */ public function isInitialized() { return $this->check(self::INITIALIZED); } /** * Returns the state of the INSIDEBLOCK flag. * * @return bool */ public function isExecuting() { return $this->check(self::INSIDEBLOCK); } /** * Returns the state of the CAS flag. * * @return bool */ public function isCAS() { return $this->check(self::CAS); } /** * Returns if WATCH is allowed in the current state. * * @return bool */ public function isWatchAllowed() { return $this->check(self::INITIALIZED) && !$this->check(self::CAS); } /** * Returns the state of the WATCH flag. * * @return bool */ public function isWatching() { return $this->check(self::WATCH); } /** * Returns the state of the DISCARDED flag. * * @return bool */ public function isDiscarded() { return $this->check(self::DISCARDED); } } /** * Client-side abstraction of a Redis transaction based on MULTI / EXEC. * * {@inheritdoc} * * @author Daniele Alessandri <suppakilla@gmail.com> */ class MultiExec implements ClientContextInterface { private $state; protected $client; protected $commands; protected $exceptions = true; protected $attempts = 0; protected $watchKeys = array(); protected $modeCAS = false; /** * @param ClientInterface $client Client instance used by the transaction. * @param array $options Initialization options. */ public function __construct(ClientInterface $client, array $options = null) { $this->assertClient($client); $this->client = $client; $this->state = new MultiExecState(); $this->configure($client, $options ?: array()); $this->reset(); } /** * Checks if the passed client instance satisfies the required conditions * needed to initialize the transaction object. * * @param ClientInterface $client Client instance used by the transaction object. * * @throws NotSupportedException */ private function assertClient(ClientInterface $client) { if ($client->getConnection() instanceof AggregateConnectionInterface) { throw new NotSupportedException( 'Cannot initialize a MULTI/EXEC transaction over aggregate connections.' ); } if (!$client->getProfile()->supportsCommands(array('MULTI', 'EXEC', 'DISCARD'))) { throw new NotSupportedException( 'The current profile does not support MULTI, EXEC and DISCARD.' ); } } /** * Configures the transaction using the provided options. * * @param ClientInterface $client Underlying client instance. * @param array $options Array of options for the transaction. **/ protected function configure(ClientInterface $client, array $options) { if (isset($options['exceptions'])) { $this->exceptions = (bool) $options['exceptions']; } else { $this->exceptions = $client->getOptions()->exceptions; } if (isset($options['cas'])) { $this->modeCAS = (bool) $options['cas']; } if (isset($options['watch']) && $keys = $options['watch']) { $this->watchKeys = $keys; } if (isset($options['retry'])) { $this->attempts = (int) $options['retry']; } } /** * Resets the state of the transaction. */ protected function reset() { $this->state->reset(); $this->commands = new SplQueue(); } /** * Initializes the transaction context. */ protected function initialize() { if ($this->state->isInitialized()) { return; } if ($this->modeCAS) { $this->state->flag(MultiExecState::CAS); } if ($this->watchKeys) { $this->watch($this->watchKeys); } $cas = $this->state->isCAS(); $discarded = $this->state->isDiscarded(); if (!$cas || ($cas && $discarded)) { $this->call('MULTI'); if ($discarded) { $this->state->unflag(MultiExecState::CAS); } } $this->state->unflag(MultiExecState::DISCARDED); $this->state->flag(MultiExecState::INITIALIZED); } /** * Dynamically invokes a Redis command with the specified arguments. * * @param string $method Command ID. * @param array $arguments Arguments for the command. * * @return mixed */ public function __call($method, $arguments) { return $this->executeCommand( $this->client->createCommand($method, $arguments) ); } /** * Executes a Redis command bypassing the transaction logic. * * @param string $commandID Command ID. * @param array $arguments Arguments for the command. * * @return mixed * * @throws ServerException */ protected function call($commandID, array $arguments = array()) { $response = $this->client->executeCommand( $this->client->createCommand($commandID, $arguments) ); if ($response instanceof ErrorResponseInterface) { throw new ServerException($response->getMessage()); } return $response; } /** * Executes the specified Redis command. * * @param CommandInterface $command Command instance. * * @return $this|mixed * * @throws AbortedMultiExecException * @throws CommunicationException */ public function executeCommand(CommandInterface $command) { $this->initialize(); if ($this->state->isCAS()) { return $this->client->executeCommand($command); } $response = $this->client->getConnection()->executeCommand($command); if ($response instanceof StatusResponse && $response == 'QUEUED') { $this->commands->enqueue($command); } elseif ($response instanceof ErrorResponseInterface) { throw new AbortedMultiExecException($this, $response->getMessage()); } else { $this->onProtocolError('The server did not return a +QUEUED status response.'); } return $this; } /** * Executes WATCH against one or more keys. * * @param string|array $keys One or more keys. * * @return mixed * * @throws NotSupportedException * @throws ClientException */ public function watch($keys) { if (!$this->client->getProfile()->supportsCommand('WATCH')) { throw new NotSupportedException('WATCH is not supported by the current profile.'); } if ($this->state->isWatchAllowed()) { throw new ClientException('Sending WATCH after MULTI is not allowed.'); } $response = $this->call('WATCH', is_array($keys) ? $keys : array($keys)); $this->state->flag(MultiExecState::WATCH); return $response; } /** * Finalizes the transaction by executing MULTI on the server. * * @return MultiExec */ public function multi() { if ($this->state->check(MultiExecState::INITIALIZED | MultiExecState::CAS)) { $this->state->unflag(MultiExecState::CAS); $this->call('MULTI'); } else { $this->initialize(); } return $this; } /** * Executes UNWATCH. * * @return MultiExec * * @throws NotSupportedException */ public function unwatch() { if (!$this->client->getProfile()->supportsCommand('UNWATCH')) { throw new NotSupportedException( 'UNWATCH is not supported by the current profile.' ); } $this->state->unflag(MultiExecState::WATCH); $this->__call('UNWATCH', array()); return $this; } /** * Resets the transaction by UNWATCH-ing the keys that are being WATCHed and * DISCARD-ing pending commands that have been already sent to the server. * * @return MultiExec */ public function discard() { if ($this->state->isInitialized()) { $this->call($this->state->isCAS() ? 'UNWATCH' : 'DISCARD'); $this->reset(); $this->state->flag(MultiExecState::DISCARDED); } return $this; } /** * Executes the whole transaction. * * @return mixed */ public function exec() { return $this->execute(); } /** * Checks the state of the transaction before execution. * * @param mixed $callable Callback for execution. * * @throws InvalidArgumentException * @throws ClientException */ private function checkBeforeExecution($callable) { if ($this->state->isExecuting()) { throw new ClientException( 'Cannot invoke "execute" or "exec" inside an active transaction context.' ); } if ($callable) { if (!is_callable($callable)) { throw new InvalidArgumentException('The argument must be a callable object.'); } if (!$this->commands->isEmpty()) { $this->discard(); throw new ClientException( 'Cannot execute a transaction block after using fluent interface.' ); } } elseif ($this->attempts) { $this->discard(); throw new ClientException( 'Automatic retries are supported only when a callable block is provided.' ); } } /** * Handles the actual execution of the whole transaction. * * @param mixed $callable Optional callback for execution. * * @return array * * @throws CommunicationException * @throws AbortedMultiExecException * @throws ServerException */ public function execute($callable = null) { $this->checkBeforeExecution($callable); $execResponse = null; $attempts = $this->attempts; do { if ($callable) { $this->executeTransactionBlock($callable); } if ($this->commands->isEmpty()) { if ($this->state->isWatching()) { $this->discard(); } return null; } $execResponse = $this->call('EXEC'); if ($execResponse === null) { if ($attempts === 0) { throw new AbortedMultiExecException( $this, 'The current transaction has been aborted by the server.' ); } $this->reset(); continue; } break; } while ($attempts-- > 0); $response = array(); $commands = $this->commands; $size = count($execResponse); if ($size !== count($commands)) { $this->onProtocolError('EXEC returned an unexpected number of response items.'); } for ($i = 0; $i < $size; $i++) { $cmdResponse = $execResponse[$i]; if ($cmdResponse instanceof ErrorResponseInterface && $this->exceptions) { throw new ServerException($cmdResponse->getMessage()); } $response[$i] = $commands->dequeue()->parseResponse($cmdResponse); } return $response; } /** * Passes the current transaction object to a callable block for execution. * * @param mixed $callable Callback. * * @throws CommunicationException * @throws ServerException */ protected function executeTransactionBlock($callable) { $exception = null; $this->state->flag(MultiExecState::INSIDEBLOCK); try { call_user_func($callable, $this); } catch (CommunicationException $exception) { // NOOP } catch (ServerException $exception) { // NOOP } catch (Exception $exception) { $this->discard(); } $this->state->unflag(MultiExecState::INSIDEBLOCK); if ($exception) { throw $exception; } } /** * Helper method for protocol errors encountered inside the transaction. * * @param string $message Error message. */ private function onProtocolError($message) { // Since a MULTI/EXEC block cannot be initialized when using aggregate // connections we can safely assume that Predis\Client::getConnection() // will return a Predis\Connection\NodeConnectionInterface instance. CommunicationException::handle(new ProtocolException( $this->client->getConnection(), $message )); } } /** * Exception class that identifies a MULTI / EXEC transaction aborted by Redis. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class AbortedMultiExecException extends PredisException { private $transaction; /** * @param MultiExec $transaction Transaction that generated the exception. * @param string $message Error message. * @param int $code Error code. */ public function __construct(MultiExec $transaction, $message, $code = null) { parent::__construct($message, $code); $this->transaction = $transaction; } /** * Returns the transaction that generated the exception. * * @return MultiExec */ public function getTransaction() { return $this->transaction; } } /* --------------------------------------------------------------------------- */ namespace Predis\Session; use SessionHandlerInterface; use Predis\ClientInterface; /** * Session handler class that relies on Predis\Client to store PHP's sessions * data into one or multiple Redis servers. * * This class is mostly intended for PHP 5.4 but it can be used under PHP 5.3 * provided that a polyfill for `SessionHandlerInterface` is defined by either * you or an external package such as `symfony/http-foundation`. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Handler implements SessionHandlerInterface { protected $client; protected $ttl; /** * @param ClientInterface $client Fully initialized client instance. * @param array $options Session handler options. */ public function __construct(ClientInterface $client, array $options = array()) { $this->client = $client; if (isset($options['gc_maxlifetime'])) { $this->ttl = (int) $options['gc_maxlifetime']; } else { $this->ttl = ini_get('session.gc_maxlifetime'); } } /** * Registers this instance as the current session handler. */ public function register() { if (version_compare(PHP_VERSION, '5.4.0') >= 0) { session_set_save_handler($this, true); } else { session_set_save_handler( array($this, 'open'), array($this, 'close'), array($this, 'read'), array($this, 'write'), array($this, 'destroy'), array($this, 'gc') ); } } /** * {@inheritdoc} */ public function open($save_path, $session_id) { // NOOP return true; } /** * {@inheritdoc} */ public function close() { // NOOP return true; } /** * {@inheritdoc} */ public function gc($maxlifetime) { // NOOP return true; } /** * {@inheritdoc} */ public function read($session_id) { if ($data = $this->client->get($session_id)) { return $data; } return ''; } /** * {@inheritdoc} */ public function write($session_id, $session_data) { $this->client->setex($session_id, $this->ttl, $session_data); return true; } /** * {@inheritdoc} */ public function destroy($session_id) { $this->client->del($session_id); return true; } /** * Returns the underlying client instance. * * @return ClientInterface */ public function getClient() { return $this->client; } /** * Returns the session max lifetime value. * * @return int */ public function getMaxLifeTime() { return $this->ttl; } } /* --------------------------------------------------------------------------- */ namespace Predis\Monitor; use Iterator; use Predis\ClientInterface; use Predis\NotSupportedException; use Predis\Connection\AggregateConnectionInterface; /** * Redis MONITOR consumer. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class Consumer implements Iterator { private $client; private $valid; private $position; /** * @param ClientInterface $client Client instance used by the consumer. */ public function __construct(ClientInterface $client) { $this->assertClient($client); $this->client = $client; $this->start(); } /** * Automatically stops the consumer when the garbage collector kicks in. */ public function __destruct() { $this->stop(); } /** * Checks if the passed client instance satisfies the required conditions * needed to initialize a monitor consumer. * * @param ClientInterface $client Client instance used by the consumer. * * @throws NotSupportedException */ private function assertClient(ClientInterface $client) { if ($client->getConnection() instanceof AggregateConnectionInterface) { throw new NotSupportedException( 'Cannot initialize a monitor consumer over aggregate connections.' ); } if ($client->getProfile()->supportsCommand('MONITOR') === false) { throw new NotSupportedException("The current profile does not support 'MONITOR'."); } } /** * Initializes the consumer and sends the MONITOR command to the server. */ protected function start() { $this->client->executeCommand( $this->client->createCommand('MONITOR') ); $this->valid = true; } /** * Stops the consumer. Internally this is done by disconnecting from server * since there is no way to terminate the stream initialized by MONITOR. */ public function stop() { $this->client->disconnect(); $this->valid = false; } /** * {@inheritdoc} */ public function rewind() { // NOOP } /** * Returns the last message payload retrieved from the server. * * @return Object */ public function current() { return $this->getValue(); } /** * {@inheritdoc} */ public function key() { return $this->position; } /** * {@inheritdoc} */ public function next() { $this->position++; } /** * Checks if the the consumer is still in a valid state to continue. * * @return bool */ public function valid() { return $this->valid; } /** * Waits for a new message from the server generated by MONITOR and returns * it when available. * * @return Object */ private function getValue() { $database = 0; $client = null; $event = $this->client->getConnection()->read(); $callback = function ($matches) use (&$database, &$client) { if (2 === $count = count($matches)) { // Redis <= 2.4 $database = (int) $matches[1]; } if (4 === $count) { // Redis >= 2.6 $database = (int) $matches[2]; $client = $matches[3]; } return ' '; }; $event = preg_replace_callback('/ \(db (\d+)\) | \[(\d+) (.*?)\] /', $callback, $event, 1); @list($timestamp, $command, $arguments) = explode(' ', $event, 3); return (object) array( 'timestamp' => (float) $timestamp, 'database' => $database, 'client' => $client, 'command' => substr($command, 1, -1), 'arguments' => $arguments, ); } } /* --------------------------------------------------------------------------- */ namespace Predis\Replication; use Predis\NotSupportedException; use Predis\Command\CommandInterface; /** * Defines a strategy for master/slave replication. * * @author Daniele Alessandri <suppakilla@gmail.com> */ class ReplicationStrategy { protected $disallowed; protected $readonly; protected $readonlySHA1; /** * */ public function __construct() { $this->disallowed = $this->getDisallowedOperations(); $this->readonly = $this->getReadOnlyOperations(); $this->readonlySHA1 = array(); } /** * Returns if the specified command will perform a read-only operation * on Redis or not. * * @param CommandInterface $command Command instance. * * @return bool * * @throws NotSupportedException */ public function isReadOperation(CommandInterface $command) { if (isset($this->disallowed[$id = $command->getId()])) { throw new NotSupportedException( "The command '$id' is not allowed in replication mode." ); } if (isset($this->readonly[$id])) { if (true === $readonly = $this->readonly[$id]) { return true; } return call_user_func($readonly, $command); } if (($eval = $id === 'EVAL') || $id === 'EVALSHA') { $sha1 = $eval ? sha1($command->getArgument(0)) : $command->getArgument(0); if (isset($this->readonlySHA1[$sha1])) { if (true === $readonly = $this->readonlySHA1[$sha1]) { return true; } return call_user_func($readonly, $command); } } return false; } /** * Returns if the specified command is not allowed for execution in a master * / slave replication context. * * @param CommandInterface $command Command instance. * * @return bool */ public function isDisallowedOperation(CommandInterface $command) { return isset($this->disallowed[$command->getId()]); } /** * Checks if a SORT command is a readable operation by parsing the arguments * array of the specified commad instance. * * @param CommandInterface $command Command instance. * * @return bool */ protected function isSortReadOnly(CommandInterface $command) { $arguments = $command->getArguments(); return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE'; } /** * Marks a command as a read-only operation. * * When the behavior of a command can be decided only at runtime depending * on its arguments, a callable object can be provided to dynamically check * if the specified command performs a read or a write operation. * * @param string $commandID Command ID. * @param mixed $readonly A boolean value or a callable object. */ public function setCommandReadOnly($commandID, $readonly = true) { $commandID = strtoupper($commandID); if ($readonly) { $this->readonly[$commandID] = $readonly; } else { unset($this->readonly[$commandID]); } } /** * Marks a Lua script for EVAL and EVALSHA as a read-only operation. When * the behaviour of a script can be decided only at runtime depending on * its arguments, a callable object can be provided to dynamically check * if the passed instance of EVAL or EVALSHA performs write operations or * not. * * @param string $script Body of the Lua script. * @param mixed $readonly A boolean value or a callable object. */ public function setScriptReadOnly($script, $readonly = true) { $sha1 = sha1($script); if ($readonly) { $this->readonlySHA1[$sha1] = $readonly; } else { unset($this->readonlySHA1[$sha1]); } } /** * Returns the default list of disallowed commands. * * @return array */ protected function getDisallowedOperations() { return array( 'SHUTDOWN' => true, 'INFO' => true, 'DBSIZE' => true, 'LASTSAVE' => true, 'CONFIG' => true, 'MONITOR' => true, 'SLAVEOF' => true, 'SAVE' => true, 'BGSAVE' => true, 'BGREWRITEAOF' => true, 'SLOWLOG' => true, ); } /** * Returns the default list of commands performing read-only operations. * * @return array */ protected function getReadOnlyOperations() { return array( 'EXISTS' => true, 'TYPE' => true, 'KEYS' => true, 'SCAN' => true, 'RANDOMKEY' => true, 'TTL' => true, 'GET' => true, 'MGET' => true, 'SUBSTR' => true, 'STRLEN' => true, 'GETRANGE' => true, 'GETBIT' => true, 'LLEN' => true, 'LRANGE' => true, 'LINDEX' => true, 'SCARD' => true, 'SISMEMBER' => true, 'SINTER' => true, 'SUNION' => true, 'SDIFF' => true, 'SMEMBERS' => true, 'SSCAN' => true, 'SRANDMEMBER' => true, 'ZRANGE' => true, 'ZREVRANGE' => true, 'ZRANGEBYSCORE' => true, 'ZREVRANGEBYSCORE' => true, 'ZCARD' => true, 'ZSCORE' => true, 'ZCOUNT' => true, 'ZRANK' => true, 'ZREVRANK' => true, 'ZSCAN' => true, 'ZLEXCOUNT' => true, 'ZRANGEBYLEX' => true, 'HGET' => true, 'HMGET' => true, 'HEXISTS' => true, 'HLEN' => true, 'HKEYS' => true, 'HVALS' => true, 'HGETALL' => true, 'HSCAN' => true, 'PING' => true, 'AUTH' => true, 'SELECT' => true, 'ECHO' => true, 'QUIT' => true, 'OBJECT' => true, 'BITCOUNT' => true, 'TIME' => true, 'PFCOUNT' => true, 'SORT' => array($this, 'isSortReadOnly'), ); } } /* --------------------------------------------------------------------------- */ // phpcs:enable
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Генерация страницы: 0.01 |
proxy
|
phpinfo
|
Настройка