diff --git a/.htaccess b/.htaccess new file mode 100755 index 0000000..9ce296e --- /dev/null +++ b/.htaccess @@ -0,0 +1,7 @@ +RewriteEngine on + +RewriteRule ^service$ /ajax.php +RewriteRule ^auth$ /auth.php +RewriteRule ^help$ /help.php +RewriteRule ^ucenter$ /ucenter.php + diff --git a/404.php b/404.php new file mode 100644 index 0000000..a664e6e --- /dev/null +++ b/404.php @@ -0,0 +1,27 @@ + + + + + + 404 | YAO + + + +
+ +
+
+

+
+
+ +
+
+ + + diff --git a/AgentManager.class.php b/AgentManager.class.php new file mode 100755 index 0000000..a987bb8 --- /dev/null +++ b/AgentManager.class.php @@ -0,0 +1,92 @@ +get('ip'); + $alias = $agent->get('alias'); + $cluster = $agent->getInt('cluster'); + $token = $agent->get('token'); + + $key_values = array( + 'ip' => '?', 'alias' => '?', 'cluster' => '?', 'token' => '?' + ); + $builder = new SQLBuilder(); + $builder->insert(self::$table, $key_values); + $sql = $builder->build(); + $params = array(ip2long($ip), $alias, $cluster, $token); + return (new MysqlPDO())->execute($sql, $params); + } + + /* */ + public static function gets(CRObject $rule) + { + $offset = $rule->getInt('offset', 0); + $limit = $rule->getInt('limit', -1); + $selected_rows = array(); + $where = array(); + $params = array(); + $order_by = array('ip' => 'ASC'); + $builder = new SQLBuilder(); + $builder->select(self::$table, $selected_rows); + $builder->where($where); + $builder->order($order_by); + $builder->limit($offset, $limit); + $sql = $builder->build(); + $agents = (new MysqlPDO())->executeQuery($sql, $params); + return $agents; + } + + /* */ + public static function count(CRObject $rule) + { + $selected_rows = array('COUNT(1) as cnt'); + $where = array(); + $params = array(); + $builder = new SQLBuilder(); + $builder->select(self::$table, $selected_rows); + $builder->where($where); + $sql = $builder->build(); + $res = (new MysqlPDO())->executeQuery($sql, $params); + return $res === null ? 0 : intval($res[0]['cnt']); + } + + /* get agent by ip */ + public static function getByIP($ip) + { + $selected_rows = array(); + $where = array('ip' => '?'); + $params = array(ip2long($ip)); + $builder = new SQLBuilder(); + $builder->select(self::$table, $selected_rows); + $builder->where($where); + $sql = $builder->build(); + $agents = (new MysqlPDO())->executeQuery($sql, $params); + return $agents !== null && count($agents) === 1 ? $agents[0] : null; + } + + /* */ + public static function remove(CRObject $agent) + { + $id = $agent->getInt('id'); + $where = array('id' => '?'); + $builder = new SQLBuilder(); + $builder->delete(self::$table); + $builder->where($where); + $sql = $builder->build(); + $params = array($id); + return (new MysqlPDO())->execute($sql, $params); + } + +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..69c69d4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## 0.0.1-alpha (2019-01-08) diff --git a/Code.class.php b/Code.class.php new file mode 100755 index 0000000..bb4b04d --- /dev/null +++ b/Code.class.php @@ -0,0 +1,73 @@ +get('name'); + $virtual_cluster = $job->getInt('virtual_cluster'); + $run_before = $job->getInt('run_before'); + $tasks = $job->get('tasks'); + $image = $job->get('image'); + $workspace = $job->getInt('workspace'); + $priority = $job->getInt('priority'); + $created_at = time(); + $created_by = $job->getInt('created_by'); + + $key_values = array( + 'name' => '?', 'image' => '?', 'workspace' => '?', 'virtual_cluster' => '?', 'priority' => '?', + 'run_before' => '?', 'created_at' => '?', 'created_by' => '?', 'tasks' => '?' + ); + $builder = new SQLBuilder(); + $builder->insert(self::$table, $key_values); + $sql = $builder->build(); + $params = array($name, $image, $workspace, $virtual_cluster, $priority, $run_before, $created_at, $created_by, $tasks); + return (new MysqlPDO())->execute($sql, $params); + } + + /* */ + public static function gets(CRObject $rule) + { + $virtual_cluster = $rule->getInt('virtual_cluster', null); + $status = $rule->getInt('status', null); + $offset = $rule->getInt('offset', 0); + $limit = $rule->getInt('limit', -1); + $selected_rows = array(); + $where = array(); + $params = array(); + if ($virtual_cluster !== null) { + $where['virtual_cluster'] = '?'; + $params[] = $virtual_cluster; + } + if ($status !== null) { + $where['status'] = '?'; + $params[] = $status; + } + $order_by = array('created_at' => 'DESC'); + $builder = new SQLBuilder(); + $builder->select(self::$table, $selected_rows); + $builder->where($where); + $builder->order($order_by); + $builder->limit($offset, $limit); + $sql = $builder->build(); + $jobs = (new MysqlPDO())->executeQuery($sql, $params); + return $jobs; + } + + /* */ + public static function count(CRObject $rule) + { + $virtual_cluster = $rule->getInt('virtual_cluster', null); + $status = $rule->getInt('status', null); + $selected_rows = array('COUNT(1) as cnt'); + $where = array(); + $params = array(); + if ($virtual_cluster !== null) { + $where['virtual_cluster'] = '?'; + $params[] = $virtual_cluster; + } + if ($status !== null) { + $where['status'] = '?'; + $params[] = $status; + } + $builder = new SQLBuilder(); + $builder->select(self::$table, $selected_rows); + $builder->where($where); + $sql = $builder->build(); + $res = (new MysqlPDO())->executeQuery($sql, $params); + return $res === null ? 0 : intval($res[0]['cnt']); + } + + /* get job by id */ + public static function get(CRObject $rule) + { + $id = $rule->getInt('id'); + $selected_rows = array(); + $where = array('id' => '?'); + $params = array($id); + $builder = new SQLBuilder(); + $builder->select(self::$table, $selected_rows); + $builder->where($where); + $sql = $builder->build(); + $jobs = (new MysqlPDO())->executeQuery($sql, $params); + return $jobs !== null && count($jobs) === 1 ? $jobs[0] : null; + } + + /* */ + public static function update(CRObject $link) + { + $id = $link->getId('id'); + $url = $link->get('url', ''); + $remark = $link->get('remark'); + $valid_from = $link->getInt('valid_from'); + $valid_to = $link->getInt('valid_to'); + $status = $link->getInt('status', 0); + + $key_values = array( + 'url' => '?', 'remark' => '?', 'valid_from' => '?', 'valid_to' => '?', 'status' => '?' + ); + $where = array('token' => '?'); + $builder = new SQLBuilder(); + $builder->update(self::$table, $key_values); + $builder->where($where); + $sql = $builder->build(); + $params = array($url, $remark, $valid_from, $valid_to, $status, $token); + return (new MysqlPDO())->execute($sql, $params); + } + +} diff --git a/README.md b/README.md index 1b313ae..dcdb1f2 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# YAO-portal \ No newline at end of file +# YAO-Portal \ No newline at end of file diff --git a/Securer.class.php b/Securer.class.php new file mode 100755 index 0000000..4c0ca9f --- /dev/null +++ b/Securer.class.php @@ -0,0 +1,36 @@ +userAgent = $config->get('User-Agent', $this->userAgent); + $this->accept = $config->get('Accept', $this->accept); + $this->acceptEncoding = $config->get('Accept-Encoding', $this->acceptEncoding); + $this->acceptLanguage = $config->get('Accept-Encoding', $this->acceptLanguage); + $this->cookie = $config->get('Cookie', $this->cookie); + $this->referer = $config->get('Referer', $this->referer); + $this->timeout = $config->get('timeout', $this->timeout); + } + + public function doGet($url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout - 2); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:')); //避免data数据过长问题 + curl_setopt($ch, CURLOPT_POST, false); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_HEADER, 1); + $ret = curl_exec($ch); + $err = curl_error($ch); + if (!$err) { + $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $header = substr($ret, 0, $header_size); + $headers = array(); + // Split the string on every "double" new line. + $arrRequests = explode("\r\n\r\n", $header); + // Loop of response headers. The "count() -1" is to avoid an empty row for the extra line break before the body of the response. + for ($index = 0; $index < count($arrRequests) - 1; $index++) { + foreach (explode("\r\n", $arrRequests[$index]) as $i => $line) { + if ($i === 0) + $headers[$index]['http_code'] = $line; + else { + list ($key, $value) = explode(': ', $line); + $headers[$index][$key] = $value; + } + } + } + $this->headers = $headers[max(0, count($headers) - 1)]; + $this->body = substr($ret, $header_size); + $this->info = curl_getinfo($ch); + } + return !$err; + } + + /* + * + * @param $url string + * @param $post_data array('key' => 'value') + * + * */ + public function doPost($url, $post_data) + { + $fields_string = http_build_query($post_data); + //open connection + $ch = curl_init(); + //set the url, number of POST vars, POST data + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout - 2); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string); + curl_setopt($ch, CURLOPT_HEADER, 1); + + $ret = curl_exec($ch); + $err = curl_error($ch); + if (!$err) { + $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $header = substr($ret, 0, $header_size); + $headers = array(); + // Split the string on every "double" new line. + $arrRequests = explode("\r\n\r\n", $header); + // Loop of response headers. The "count() -1" is to avoid an empty row for the extra line break before the body of the response. + for ($index = 0; $index < count($arrRequests) - 1; $index++) { + foreach (explode("\r\n", $arrRequests[$index]) as $i => $line) { + if ($i === 0) + $headers[$index]['http_code'] = $line; + else { + list ($key, $value) = explode(': ', $line); + $headers[$index][$key] = $value; + } + } + } + $this->headers = $headers[max(0, count($headers) - 1)]; + $this->body = substr($ret, $header_size); + $this->info = curl_getinfo($ch); + } + return !$err; + } + + public function getHeader($key) + { + return $key; + } + + public function getBody() + { + return $this->body; + } + + public function getStatusCode() + { + return $this->info['http_code']; + } +} \ No newline at end of file diff --git a/UserManager.class.php b/UserManager.class.php new file mode 100644 index 0000000..9ee1bc9 --- /dev/null +++ b/UserManager.class.php @@ -0,0 +1,57 @@ +get('open_id'); + $email = $user->get('email'); + $role = $user->get('role'); + $level = $user->getInt('level', 0); + if ($email !== null && !Validator::isEmail($email)) { + return false; + } + $key_values = array('open_id' => '?', 'email' => '?', 'role' => '?', 'time' => '?', 'level' => '?'); + $builder = new SQLBuilder(); + $builder->insert('yao_user', $key_values); + $sql = $builder->build(); + $params = array($open_id, $email, $role, time(), $level); + return (new MysqlPDO())->execute($sql, $params); + } + + /**/ + public static function getByUID($uid) + { + $selected_rows = array(); + $where_arr = array('uid' => '?'); + $builder = new SQLBuilder(); + $builder->select('yao_user', $selected_rows); + $builder->where($where_arr); + $sql = $builder->build(); + $params = array($uid); + $users = (new MysqlPDO())->executeQuery($sql, $params); + return $users !== null && count($users) === 1 ? $users[0] : null; + } + + /**/ + public static function getByOpenID($open_id) + { + $selected_rows = array(); + $where_arr = array('open_id' => '?'); + $builder = new SQLBuilder(); + $builder->select('yao_user', $selected_rows); + $builder->where($where_arr); + $sql = $builder->build(); + $params = array($open_id); + $users = (new MysqlPDO())->executeQuery($sql, $params); + return $users !== null && count($users) === 1 ? $users[0] : null; + } +} \ No newline at end of file diff --git a/agent.logic.php b/agent.logic.php new file mode 100644 index 0000000..8f0367f --- /dev/null +++ b/agent.logic.php @@ -0,0 +1,65 @@ +get('ip')) !== null) { + $res['errno'] = Code::RECORD_ALREADY_EXIST; + } else { + $token = Random::randomString(32); + $agent->set('token', $token); + $res['errno'] = AgentManager::add($agent) ? Code::SUCCESS : Code::UNKNOWN_ERROR; + } + $log = new CRObject(); + $log->set('scope', Session::get('uid')); + $log->set('tag', 'agent.add'); + $content = array('agent' => $agent, 'response' => $res['errno']); + $log->set('content', json_encode($content)); + CRLogger::log($log); + return $res; +} + +function agent_remove(CRObject $agent) +{ + if (!AccessController::hasAccess(Session::get('role', 'visitor'), 'agent.remove')) { + $res['errno'] = Code::NO_PRIVILEGE; + return $res; + } + $res['errno'] = AgentManager::remove($agent) ? Code::SUCCESS : Code::UNKNOWN_ERROR; + $log = new CRObject(); + $log->set('scope', Session::get('uid')); + $log->set('tag', 'agent.remove'); + $content = array('agent' => $agent, 'response' => $res['errno']); + $log->set('content', json_encode($content)); + CRLogger::log($log); + return $res; +} + +function agent_list(CRObject $rule) +{ + if (!AccessController::hasAccess(Session::get('role', 'visitor'), 'agent.list')) { + $res['errno'] = Code::NO_PRIVILEGE; + return $res; + } + $res['agents'] = AgentManager::gets($rule); + $res['count'] = AgentManager::count($rule); + $res['errno'] = $res['agents'] === null ? Code::FAIL : Code::SUCCESS; + return $res; +} diff --git a/ajax.php b/ajax.php new file mode 100644 index 0000000..114d78b --- /dev/null +++ b/ajax.php @@ -0,0 +1,136 @@ + Code::UNKNOWN_REQUEST); + +$action = cr_get_GET('action'); + +if (!csrf_check($action)) { + $res['errno'] = 99; + $res['msg'] = 'invalid csrf_token'; + print_response($res); + exit(0); +} + +switch ($action) { + case 'job_list': + $rule = new CRObject(); + $rule->set('who', cr_get_GET('who', 'self')); + $rule->set('offset', cr_get_GET('offset')); + $rule->set('limit', cr_get_GET('limit')); + $rule->set('order', 'latest'); + $res = job_list($rule); + break; + + case 'job_submit': + $job = new CRObject(); + $job->set('name', cr_get_POST('name')); + $job->set('virtual_cluster', cr_get_POST('cluster')); + $job->set('workspace', cr_get_POST('workspace')); + $job->set('priority', cr_get_POST('priority')); + $job->set('image', cr_get_POST('image')); + $job->set('run_before', cr_get_POST('run_before')); + $job->set('tasks', cr_get_POST('tasks')); + $res = job_submit($job); + break; + + case 'job_stop': + $job = new CRObject(); + $job->set('id', cr_get_POST('id')); + $res = job_stop($link); + break; + + case 'job_describe': + $job = new CRObject(); + $job->set('id', cr_get_POST('id')); + $res = job_describe($job); + break; + + case 'agent_list': + $rule = new CRObject(); + $rule->set('offset', cr_get_GET('offset')); + $rule->set('limit', cr_get_GET('limit')); + $res = agent_list($rule); + break; + + case 'agent_add': + $agent = new CRObject(); + $agent->set('ip', cr_get_POST('ip')); + $agent->set('alias', cr_get_POST('alias')); + $agent->set('cluster', cr_get_POST('cluster')); + $res = agent_add($agent); + break; + + case 'agent_remove': + $job = new CRObject(); + $job->set('id', cr_get_POST('id')); + $res = agent_remove($job); + break; + + case 'user_signout': + $res = user_signout(); + break; + + case 'log_gets': + $rule = new CRObject(); + $rule->set('who', cr_get_GET('who', 'self')); + $rule->set('offset', cr_get_GET('offset')); + $rule->set('limit', cr_get_GET('limit')); + $rule->set('order', 'latest'); + $res = log_gets($rule); + break; + + case 'oauth_get_url': + $res = oauth_get_url(); + break; + + default: + break; +} + +print_response($res); diff --git a/auth.php b/auth.php new file mode 100755 index 0000000..26bf0b0 --- /dev/null +++ b/auth.php @@ -0,0 +1,89 @@ + 'authorization_code', + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'code' => $_GET['code'], + 'redirect_uri' => BASE_URL . '/auth', +); + +$spider = new Spider(); +$spider->doPost($url, $fields); +$result = json_decode($spider->getBody(), true); +$token = $result['token']; + +$url = OAUTH_SITE . '/api?action=get_info'; +$fields = array( + 'api_name' => 'basic', + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'token' => $token +); +$spider = new Spider(); +$spider->doPost($url, $fields); +$response = json_decode($spider->getBody(), true); + +if ($response['errno'] === 0) { + $info = $response['info']; + $open_id = ($info !== null && isset($info['open_id'])) ? $info['open_id'] : null; + $email = ($info !== null && isset($info['email'])) ? $info['email'] : null; + $role = ($info !== null && isset($info['role'])) ? $info['role'] : 'normal'; + $nickname = ($info !== null && isset($info['nickname'])) ? $info['nickname'] : 'u2913'; + + $user = new CRObject(); + $user->set('open_id', $open_id); + $user->set('email', $email); + $user->set('role', $role); + $res = user_get($user); + + if ($res['errno'] === 0) { + $user = $res['user']; + Session::put('uid', $user['uid']); + Session::put('role', $user['role']); + Session::put('nickname', $nickname); + + $log = new CRObject(); + $log->set('scope', $user['uid']); + $log->set('tag', 'user.login'); + $content = array('uid' => $user['uid'], 'response' => $res['errno']); + $log->set('content', json_encode($content)); + CRLogger::log($log); + + header('location:' . BASE_URL . '/ucenter'); + exit; + } else { + echo Code::getErrorMsg($res['errno']); + exit; + } +} + +echo $response['msg']; +exit; diff --git a/config-sample.inc.php b/config-sample.inc.php new file mode 100755 index 0000000..5014962 --- /dev/null +++ b/config-sample.inc.php @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/global.inc.php b/global.inc.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/global.inc.php @@ -0,0 +1 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/header.php b/header.php new file mode 100755 index 0000000..46ea6fb --- /dev/null +++ b/header.php @@ -0,0 +1,40 @@ + + \ No newline at end of file diff --git a/help.php b/help.php new file mode 100755 index 0000000..89516e1 --- /dev/null +++ b/help.php @@ -0,0 +1,84 @@ + + + + + + Help | YAO + + +
+ + +
+
+ +
+
+
YAO
+
+

Yet Another Octopus

+
+
+
+
About
+
+
    +
  • one
  • +
  • two
  • +
+
+
+
+
TOS
+
+

Term of service

+
+
+
+
Privacy
+
+

Privacy

+
+
+
+
Feedback
+
+

Contact with + +

+
+
+ +
+
+
+ +
+
+ + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..092ce6d --- /dev/null +++ b/index.php @@ -0,0 +1,28 @@ + + + + + + Yet Another Octopus | YAO + + + +
+ + +
+ +
+

YAO-Yet Another Octopus

+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/init.inc.php b/init.inc.php new file mode 100644 index 0000000..f654d47 --- /dev/null +++ b/init.inc.php @@ -0,0 +1,95 @@ +set('host', DB_HOST); + $config->set('port', DB_PORT); + $config->set('db', DB_NAME); + $config->set('user', DB_USER); + $config->set('password', DB_PASSWORD); + $config->set('show_error', DB_SHOW_ERROR); + MysqlPDO::configure($config); +} + +function init_redis() +{ + $config = new CRObject(); + $config->set('scheme', REDIS_SCHEME); + $config->set('host', REDIS_HOST); + $config->set('port', REDIS_PORT); + $config->set('show_error', REDIS_SHOW_ERROR); + RedisDAO::configure($config); +} + +function init_logger() +{ + $config = new CRObject(); + $config->set('db_table', 'yao_log'); + CRLogger::configure($config); +} + +function init_Session() +{ + $config = new CRObject(); + $config->set('time_out', SESSION_TIME_OUT); + $config->set('bind_ip', BIND_SESSION_WITH_IP); + $config->set('PK', 'username'); + Session::configure($config); +} + +function init_accessMap() +{ + // $operation => array of roles + $map = array( + /* user */ + 'user.get' => array('root', 'admin', 'developer', 'normal'), + 'user.get_others' => array('root', 'admin'), + + /* logs */ + 'logs.get' => array('root', 'admin', 'developer', 'normal'), + 'logs.get_others' => array('root', 'admin'), + + /* job */ + 'job.list' => array('root', 'admin', 'developer', 'normal'), + 'job.submit' => array('root', 'admin', 'developer', 'normal'), + + /* agent */ + 'agent.list' => array('root', 'admin', 'normal'), + 'agent.add' => array('root', 'admin'), + 'agent.remove' => array('root', 'admin'), + + /* ucenter entry show control */ + 'ucenter.home' => array('root', 'admin', 'developer', 'normal'), + 'ucenter.jobs' => array('root', 'admin', 'developer', 'normal'), + 'ucenter.workspaces' => array('root', 'admin', 'developer', 'normal'), + 'ucenter.jobs_all' => array('root', 'admin'), + 'ucenter.workspaces_all' => array('root', 'admin'), + 'ucenter.logs' => array('root', 'admin', 'developer', 'normal'), + 'ucenter.logs_all' => array('root', 'admin'), + 'ucenter.agents' => array('root', 'admin'), + 'ucenter.clusters' => array('root', 'admin'), + 'ucenter.admin' => array('root', 'admin'), + 'ucenter.users' => array('root', 'admin'), + 'ucenter.resources' => array('root', 'admin'), + 'ucenter.summary' => array('root', 'admin'), + 'ucenter.visitors' => array('root', 'admin') + ); + AccessController::setMap($map); +} \ No newline at end of file diff --git a/install.php b/install.php new file mode 100755 index 0000000..66af5f5 --- /dev/null +++ b/install.php @@ -0,0 +1,197 @@ +set('show_error', true); +MysqlPDO::configure($config); + + +create_table_user(); +create_table_workspace(); +create_table_cluster(); +create_table_job(); +create_table_agent(); +create_table_resource(); +create_table_model(); +create_table_log(); + +function execute_sqls($sqls) +{ + foreach ($sqls as $description => $sql) { + echo "Executing $description: "; + $res = (new MysqlPDO)->execute($sql, array()); + echo $res ? 'Success' : 'Failed'; + echo "
"; + } +} + +function create_table_user() +{ + $sqls = array( +// 'DROP `yao_user`' => +// 'DROP TABLE IF EXISTS `yao_user`', + 'CREATE `yao_user`' => + 'CREATE TABLE `yao_user`( + `uid` int AUTO_INCREMENT, + PRIMARY KEY (`uid`), + `open_id` varchar(64) NOT NULL, + UNIQUE (`open_id`), + `email` varchar(64), + `role` varchar(12) NOT NULL, + `level` int DEFAULT 0, + `time` bigint + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci' + ); + execute_sqls($sqls); +} + +function create_table_workspace() +{ + $sqls = array( +// 'DROP `yao_workspace`' => 'DROP TABLE IF EXISTS `yao_workspace`', + 'CREATE `yao_workspace`' => + 'CREATE TABLE `yao_workspace`( + `id` int AUTO_INCREMENT, + PRIMARY KEY(`id`), + `name` varchar(64) NOT NULL, + `content` json NOT NULL, + `created_at` BIGINT NOT NULL, + `virtual_cluster` varchar(64) NOT NULL, + INDEX(`virtual_cluster`), + `created_by` int NOT NULL, + `permission` int, /* [0, 1, 2] * 10 + [0, 1, 2] => [-, r, w] * [-, r, w] */ + `version` int NOT NULL DEFAULT 0 /* for upgrade purpose */ + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci', + ); + execute_sqls($sqls); +} + +function create_table_cluster() +{ + $sqls = array( +// 'DROP `yao_cluster`' => 'DROP TABLE IF EXISTS `yao_cluster`', + 'CREATE `yao_cluster`' => + 'CREATE TABLE `yao_cluster`( + `name` VARCHAR(64) NOT NULL, + PRIMARY KEY(`name`), + `created_at` BIGINT NOT NULL, + `created_by` int NOT NULL, + `reserved_nodes` json NOT NULL, + `quota_per_day` int NOT NULL, + `quota_used` int NOT NULL, + + `version` int NOT NULL DEFAULT 0 /* for upgrade purpose */ + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci', + ); + execute_sqls($sqls); +} + +function create_table_resource() +{ + $sqls = array( +// 'DROP `yao_resource`' => 'DROP TABLE IF EXISTS `yao_resource`', + 'CREATE `yao_resource`' => + 'CREATE TABLE `yao_resource`( + `id` int AUTO_INCREMENT, + PRIMARY KEY(`id`), + `ip` bigint NOT NULL, + `type` int NOT NULL, /* 0-CPU, 1-GPU */ + `model` VARCHAR(64) NOT NULL, /* eg. i7, P100 */ + `memory` int NOT NULL /* MB */ + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci', + ); + execute_sqls($sqls); +} + +function create_table_agent() +{ + $sqls = array( +// 'DROP `yao_agent`' => 'DROP TABLE IF EXISTS `yao_agent`', + 'CREATE `yao_agent`' => + 'CREATE TABLE `yao_agent`( + `id` int AUTO_INCREMENT, + PRIMARY KEY(`id`), + `ip` bigint NOT NULL, + UNIQUE(`ip`), + `alias` VARCHAR(64), + `cluster` int NOT NULL DEFAULT 0, + `token` VARCHAR(64) NOT NULL + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci', + ); + execute_sqls($sqls); +} + +function create_table_model() +{ + $sqls = array( +// 'DROP `yao_model`' => 'DROP TABLE IF EXISTS `yao_model`', + 'CREATE `yao_model`' => + 'CREATE TABLE `yao_model`( + `id` int AUTO_INCREMENT, + PRIMARY KEY(`id`), + `virtual_cluster` varchar(64) NOT NULL, + `name` varchar(64) NOT NULL, + `content` json NOT NULL, + `created_at` BIGINT NOT NULL, + `created_by` int NOT NULL, + `permission` int /* [0, 1, 2] * 10 + [0, 1, 2] => [-, r, w] * [-, r, w] */ + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci', + ); + execute_sqls($sqls); +} + +function create_table_job() +{ + $sqls = array( +// 'DROP `yao_job`' => 'DROP TABLE IF EXISTS `yao_job`', + 'CREATE `yao_job`' => + 'CREATE TABLE `yao_job`( + `id` int AUTO_INCREMENT, + PRIMARY KEY(`id`), + `name` varchar(64) NOT NULL, + `image` varchar(256) NOT NULL, + `tasks` json NOT NULL, + `workspace` int NOT NULL, + `virtual_cluster` int NOT NULL DEFAULT 0, + `priority` int NOT NULL DEFAULT 0, + `run_before` bigint, + `status` int NOT NULL DEFAULT 0,/* 0-submitted, 1-running, 2-finished, 3-failed, 4-stopped */ + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT, + `created_by` int, + `version` int NOT NULL DEFAULT 0 /* for upgrade purpose */ + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci', + ); + execute_sqls($sqls); +} + +function create_table_log() +{ + $sqls = array( +// 'DROP `yao_log`' => 'DROP TABLE IF EXISTS `yao_log`', + 'CREATE `yao_log`' => + 'CREATE TABLE `yao_log`( + `id` BIGINT AUTO_INCREMENT, + PRIMARY KEY(`id`), + `scope` VARCHAR(128) NOT NULL, + INDEX(`scope`), + `tag` VARCHAR(128) NOT NULL, + INDEX(`tag`), + `level` INT NOT NULL, /* too small value sets, no need to index */ + `time` BIGINT NOT NULL, + INDEX(`time`), + `ip` BIGINT NOT NULL, + INDEX(`ip`), + `content` json + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci' + ); + execute_sqls($sqls); +} diff --git a/job.logic.php b/job.logic.php new file mode 100644 index 0000000..add7c24 --- /dev/null +++ b/job.logic.php @@ -0,0 +1,95 @@ +set('created_by', Session::get('uid')); + $res['errno'] = JobManager::add($job) ? Code::SUCCESS : Code::UNKNOWN_ERROR; + $log = new CRObject(); + $log->set('scope', Session::get('uid')); + $log->set('tag', 'job.submit'); + $content = array('job' => $job, 'response' => $res['errno']); + $log->set('content', json_encode($content)); + CRLogger::log($log); + /* TODO notify scheduler */ + return $res; +} + +function job_stop(CRObject $job) +{ + if (!AccessController::hasAccess(Session::get('role', 'visitor'), 'job.stop')) { + $res['errno'] = Code::NO_PRIVILEGE; + return $res; + } + $origin = JobManager::get($job); + if ($origin === null) { + $res['errno'] = Code::RECORD_NOT_EXIST; + } else if ($origin['created_by'] !== Session::get('uid') && !AccessController::hasAccess(Session::get('role', 'visitor'), 'job.stop_others')) { + $res['errno'] = Code::NO_PRIVILEGE; + } else if ($origin['status'] !== '0' && $origin['status'] !== '1') { + $res['errno'] = Code::RECORD_REMOVED; + } else { + $origin['status'] = 4; + $res['errno'] = JobManager::update(new CRObject($origin)) ? Code::SUCCESS : Code::UNKNOWN_ERROR; + } + $log = new CRObject(); + $log->set('scope', Session::get('uid')); + $log->set('tag', 'job.stop'); + $content = array('id' => $job->getInt('id'), 'response' => $res['errno']); + $log->set('content', json_encode($content)); + CRLogger::log($log); + return $res; +} + +function job_list(CRObject $rule) +{ + if (!AccessController::hasAccess(Session::get('role', 'visitor'), 'job.list')) { + $res['errno'] = Code::NO_PRIVILEGE; + return $res; + } + if ($rule->get('who') !== 'all') { + $rule->set('who', 'self'); + $rule->set('created_by', Session::get('uid')); + } + if ($rule->get('who') === 'all' && !AccessController::hasAccess(Session::get('role', 'visitor'), 'job.list_others')) { + $res['errno'] = Code::NO_PRIVILEGE; + return $res; + } + $res['jobs'] = JobManager::gets($rule); + $res['count'] = JobManager::count($rule); + $res['errno'] = $res['jobs'] === null ? Code::FAIL : Code::SUCCESS; + return $res; +} + +function job_describe(CRObject $rule) +{ + if (!AccessController::hasAccess(Session::get('role', 'visitor'), 'job.describe')) { + $res['errno'] = Code::NO_PRIVILEGE; + return $res; + } + $res['errno'] = Code::FAIL; + $origin = JobManager::get($rule); + if ($origin === null) { + $res['errno'] = Code::RECORD_NOT_EXIST; + } else if ($origin['created_by'] !== Session::get('uid') && !AccessController::hasAccess(Session::get('role', 'visitor'), 'job.describe_others')) { + $res['errno'] = Code::NO_PRIVILEGE; + } + return $res; +} diff --git a/modals.php b/modals.php new file mode 100755 index 0000000..9b19b1d --- /dev/null +++ b/modals.php @@ -0,0 +1,191 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/predis/autoload.php b/predis/autoload.php new file mode 100644 index 0000000..6b5a00b --- /dev/null +++ b/predis/autoload.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +require __DIR__.'/src/Autoloader.php'; + +Predis\Autoloader::register(); diff --git a/predis/src/Autoloader.php b/predis/src/Autoloader.php new file mode 100644 index 0000000..17ec2ff --- /dev/null +++ b/predis/src/Autoloader.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis; + +/** + * Implements a lightweight PSR-0 compliant autoloader for Predis. + * + * @author Eric Naeseth + * @author Daniele Alessandri + */ +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; + } + } + } +} diff --git a/predis/src/Client.php b/predis/src/Client.php new file mode 100644 index 0000000..efe5c8b --- /dev/null +++ b/predis/src/Client.php @@ -0,0 +1,547 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis; + +use Predis\Command\CommandInterface; +use Predis\Command\RawCommand; +use Predis\Command\ScriptCommand; +use Predis\Configuration\Options; +use Predis\Configuration\OptionsInterface; +use Predis\Connection\AggregateConnectionInterface; +use Predis\Connection\ConnectionInterface; +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; + +/** + * 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 + */ +class Client implements ClientInterface, \IteratorAggregate +{ + const VERSION = '1.1.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. + * + * @throws \InvalidArgumentException + * + * @return OptionsInterface + */ + 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. + * + * @throws \InvalidArgumentException + * + * @return ConnectionInterface + */ + 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); + } elseif ($options->defined('replication')) { + $replication = $options->replication; + + if ($replication instanceof AggregateConnectionInterface) { + $connection = $replication; + $options->connections->aggregate($connection, $parameters); + } else { + $initializer = $this->getConnectionInitializerWrapper($replication); + $connection = $initializer($parameters, $options); + } + } 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. + * + * @throws \InvalidArgumentException + * + * @return Client + */ + 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. + * + * @throws NotSupportedException + * + * @return Connection\NodeConnectionInterface + */ + 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 possible to identify 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. + * + * @throws ServerException + * + * @return mixed + */ + 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 publish/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); + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + $clients = array(); + $connection = $this->getConnection(); + + if (!$connection instanceof \Traversable) { + throw new ClientException('The underlying connection is not traversable'); + } + + foreach ($connection as $node) { + $clients[(string) $node] = new static($node, $this->getOptions()); + } + + return new \ArrayIterator($clients); + } +} diff --git a/predis/src/ClientContextInterface.php b/predis/src/ClientContextInterface.php new file mode 100644 index 0000000..deb3865 --- /dev/null +++ b/predis/src/ClientContextInterface.php @@ -0,0 +1,198 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis; + +use Predis\Command\CommandInterface; + +/** + * 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 bitfield($key, $subcommand, ...$subcommandArg) + * @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 hstrlen($key, $field) + * @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, $count = null) + * @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 zrevrangebylex($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() + * @method $this geoadd($key, $longitude, $latitude, $member) + * @method $this geohash($key, array $members) + * @method $this geopos($key, array $members) + * @method $this geodist($key, $member1, $member2, $unit = null) + * @method $this georadius($key, $longitude, $latitude, $radius, $unit, array $options = null) + * @method $this georadiusbymember($key, $member, $radius, $unit, array $options = null) + * + * @author Daniele Alessandri + */ +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); +} diff --git a/predis/src/ClientException.php b/predis/src/ClientException.php new file mode 100644 index 0000000..6c07aaf --- /dev/null +++ b/predis/src/ClientException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis; + +/** + * Exception class that identifies client-side errors. + * + * @author Daniele Alessandri + */ +class ClientException extends PredisException +{ +} diff --git a/predis/src/ClientInterface.php b/predis/src/ClientInterface.php new file mode 100644 index 0000000..d062526 --- /dev/null +++ b/predis/src/ClientInterface.php @@ -0,0 +1,239 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis; + +use Predis\Command\CommandInterface; +use Predis\Configuration\OptionsInterface; +use Predis\Connection\ConnectionInterface; +use Predis\Profile\ProfileInterface; + +/** + * 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 array bitfield($key, $subcommand, ...$subcommandArg) + * @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 int hstrlen($key, $field) + * @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, $count = null) + * @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, $max, $min, 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 array zrevrangebylex($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() + * @method int geoadd($key, $longitude, $latitude, $member) + * @method array geohash($key, array $members) + * @method array geopos($key, array $members) + * @method string geodist($key, $member1, $member2, $unit = null) + * @method array georadius($key, $longitude, $latitude, $radius, $unit, array $options = null) + * @method array georadiusbymember($key, $member, $radius, $unit, array $options = null) + * + * @author Daniele Alessandri + */ +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); +} diff --git a/predis/src/Cluster/ClusterStrategy.php b/predis/src/Cluster/ClusterStrategy.php new file mode 100644 index 0000000..1891907 --- /dev/null +++ b/predis/src/Cluster/ClusterStrategy.php @@ -0,0 +1,469 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Cluster; + +use Predis\Command\CommandInterface; +use Predis\Command\ScriptCommand; + +/** + * Common class implementing the logic needed to support clustering strategies. + * + * @author Daniele Alessandri + */ +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' => $getKeyFromAllArguments, + 'DEL' => $getKeyFromAllArguments, + 'TYPE' => $getKeyFromFirstArgument, + 'EXPIRE' => $getKeyFromFirstArgument, + 'EXPIREAT' => $getKeyFromFirstArgument, + 'PERSIST' => $getKeyFromFirstArgument, + 'PEXPIRE' => $getKeyFromFirstArgument, + 'PEXPIREAT' => $getKeyFromFirstArgument, + 'TTL' => $getKeyFromFirstArgument, + 'PTTL' => $getKeyFromFirstArgument, + 'SORT' => array($this, 'getKeyFromSortCommand'), + '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, + 'BITFIELD' => $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, + 'ZREVRANGEBYLEX' => $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, + 'HSTRLEN' => $getKeyFromFirstArgument, + + /* commands operating on HyperLogLog */ + 'PFADD' => $getKeyFromFirstArgument, + 'PFCOUNT' => $getKeyFromAllArguments, + 'PFMERGE' => $getKeyFromAllArguments, + + /* scripting */ + 'EVAL' => array($this, 'getKeyFromScriptingCommands'), + 'EVALSHA' => array($this, 'getKeyFromScriptingCommands'), + + /* commands performing geospatial operations */ + 'GEOADD' => $getKeyFromFirstArgument, + 'GEOHASH' => $getKeyFromFirstArgument, + 'GEOPOS' => $getKeyFromFirstArgument, + 'GEODIST' => $getKeyFromFirstArgument, + 'GEORADIUS' => array($this, 'getKeyFromGeoradiusCommands'), + 'GEORADIUSBYMEMBER' => array($this, 'getKeyFromGeoradiusCommands'), + ); + } + + /** + * 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 SORT command. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromSortCommand(CommandInterface $command) + { + $arguments = $command->getArguments(); + $firstKey = $arguments[0]; + + if (1 === $argc = count($arguments)) { + return $firstKey; + } + + $keys = array($firstKey); + + for ($i = 1; $i < $argc; ++$i) { + if (strtoupper($arguments[$i]) === 'STORE') { + $keys[] = $arguments[++$i]; + } + } + + if ($this->checkSameSlotForKeys($keys)) { + return $firstKey; + } + } + + /** + * 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 GEORADIUS and GEORADIUSBYMEMBER commands. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromGeoradiusCommands(CommandInterface $command) + { + $arguments = $command->getArguments(); + $argc = count($arguments); + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; + + if ($argc > $startIndex) { + $keys = array($arguments[0]); + + for ($i = $startIndex; $i < $argc; ++$i) { + $argument = strtoupper($arguments[$i]); + if ($argument === 'STORE' || $argument === 'STOREDIST') { + $keys[] = $arguments[++$i]; + } + } + + if ($this->checkSameSlotForKeys($keys)) { + return $arguments[0]; + } else { + return; + } + } + + return $arguments[0]; + } + + /** + * 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; + } +} diff --git a/predis/src/Cluster/Distributor/DistributorInterface.php b/predis/src/Cluster/Distributor/DistributorInterface.php new file mode 100644 index 0000000..831f52c --- /dev/null +++ b/predis/src/Cluster/Distributor/DistributorInterface.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Cluster\Distributor; + +use Predis\Cluster\Hash\HashGeneratorInterface; + +/** + * A distributor implements the logic to automatically distribute keys among + * several nodes for client-side sharding. + * + * @author Daniele Alessandri + */ +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(); +} diff --git a/predis/src/Cluster/Distributor/EmptyRingException.php b/predis/src/Cluster/Distributor/EmptyRingException.php new file mode 100644 index 0000000..039f2f2 --- /dev/null +++ b/predis/src/Cluster/Distributor/EmptyRingException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Cluster\Distributor; + +/** + * Exception class that identifies empty rings. + * + * @author Daniele Alessandri + */ +class EmptyRingException extends \Exception +{ +} diff --git a/predis/src/Cluster/Distributor/HashRing.php b/predis/src/Cluster/Distributor/HashRing.php new file mode 100644 index 0000000..db864d9 --- /dev/null +++ b/predis/src/Cluster/Distributor/HashRing.php @@ -0,0 +1,270 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Cluster\Distributor; + +use Predis\Cluster\Hash\HashGeneratorInterface; + +/** + * 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 + * @author Lorenzo Castelli + */ +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; + } +} diff --git a/predis/src/Cluster/Distributor/KetamaRing.php b/predis/src/Cluster/Distributor/KetamaRing.php new file mode 100644 index 0000000..dc77f32 --- /dev/null +++ b/predis/src/Cluster/Distributor/KetamaRing.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Cluster\Distributor; + +/** + * 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 + * @author Lorenzo Castelli + */ +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; + } +} diff --git a/predis/src/Cluster/Hash/CRC16.php b/predis/src/Cluster/Hash/CRC16.php new file mode 100644 index 0000000..3add0ce --- /dev/null +++ b/predis/src/Cluster/Hash/CRC16.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Cluster\Hash; + +/** + * Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster. + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Cluster/Hash/HashGeneratorInterface.php b/predis/src/Cluster/Hash/HashGeneratorInterface.php new file mode 100644 index 0000000..271b9e7 --- /dev/null +++ b/predis/src/Cluster/Hash/HashGeneratorInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +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 + */ +interface HashGeneratorInterface +{ + /** + * Generates an hash from a string to be used for distribution. + * + * @param string $value String value. + * + * @return int + */ + public function hash($value); +} diff --git a/predis/src/Cluster/PredisStrategy.php b/predis/src/Cluster/PredisStrategy.php new file mode 100644 index 0000000..2066842 --- /dev/null +++ b/predis/src/Cluster/PredisStrategy.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Cluster; + +use Predis\Cluster\Distributor\DistributorInterface; +use Predis\Cluster\Distributor\HashRing; + +/** + * Default cluster strategy used by Predis to handle client-side sharding. + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Cluster/RedisStrategy.php b/predis/src/Cluster/RedisStrategy.php new file mode 100644 index 0000000..df0bdb4 --- /dev/null +++ b/predis/src/Cluster/RedisStrategy.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Cluster; + +use Predis\Cluster\Hash\CRC16; +use Predis\Cluster\Hash\HashGeneratorInterface; +use Predis\NotSupportedException; + +/** + * Default class used by Predis to calculate hashes out of keys of + * commands supported by redis-cluster. + * + * @author Daniele Alessandri + */ +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' + ); + } +} diff --git a/predis/src/Cluster/StrategyInterface.php b/predis/src/Cluster/StrategyInterface.php new file mode 100644 index 0000000..cdf7d09 --- /dev/null +++ b/predis/src/Cluster/StrategyInterface.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Cluster; + +use Predis\Cluster\Distributor\DistributorInterface; +use Predis\Command\CommandInterface; + +/** + * 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 + */ +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(); +} diff --git a/predis/src/Collection/Iterator/CursorBasedIterator.php b/predis/src/Collection/Iterator/CursorBasedIterator.php new file mode 100644 index 0000000..922883f --- /dev/null +++ b/predis/src/Collection/Iterator/CursorBasedIterator.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Collection\Iterator; + +use Predis\ClientInterface; +use Predis\NotSupportedException; + +/** + * 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 + */ +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; + } +} diff --git a/predis/src/Collection/Iterator/HashKey.php b/predis/src/Collection/Iterator/HashKey.php new file mode 100644 index 0000000..078625f --- /dev/null +++ b/predis/src/Collection/Iterator/HashKey.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Collection\Iterator; + +use Predis\ClientInterface; + +/** + * 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 + * + * @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() + { + if ($kv = each($this->elements)) { + $this->position = $kv[0]; + $this->current = $kv[1]; + + unset($this->elements[$this->position]); + } + } +} diff --git a/predis/src/Collection/Iterator/Keyspace.php b/predis/src/Collection/Iterator/Keyspace.php new file mode 100644 index 0000000..5d985b9 --- /dev/null +++ b/predis/src/Collection/Iterator/Keyspace.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Collection\Iterator; + +use Predis\ClientInterface; + +/** + * 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 + * + * @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()); + } +} diff --git a/predis/src/Collection/Iterator/ListKey.php b/predis/src/Collection/Iterator/ListKey.php new file mode 100644 index 0000000..7a6eb47 --- /dev/null +++ b/predis/src/Collection/Iterator/ListKey.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Collection\Iterator; + +use Predis\ClientInterface; +use Predis\NotSupportedException; + +/** + * 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 + * + * @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; + } +} diff --git a/predis/src/Collection/Iterator/SetKey.php b/predis/src/Collection/Iterator/SetKey.php new file mode 100644 index 0000000..bf25439 --- /dev/null +++ b/predis/src/Collection/Iterator/SetKey.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Collection\Iterator; + +use Predis\ClientInterface; + +/** + * 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 + * + * @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()); + } +} diff --git a/predis/src/Collection/Iterator/SortedSetKey.php b/predis/src/Collection/Iterator/SortedSetKey.php new file mode 100644 index 0000000..e2f1789 --- /dev/null +++ b/predis/src/Collection/Iterator/SortedSetKey.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Collection\Iterator; + +use Predis\ClientInterface; + +/** + * 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 + * + * @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]); + } + } +} diff --git a/predis/src/Command/Command.php b/predis/src/Command/Command.php new file mode 100644 index 0000000..bb538e7 --- /dev/null +++ b/predis/src/Command/Command.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * Base class for Redis commands. + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/CommandInterface.php b/predis/src/Command/CommandInterface.php new file mode 100644 index 0000000..9f349e1 --- /dev/null +++ b/predis/src/Command/CommandInterface.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * Defines an abstraction representing a Redis command. + * + * @author Daniele Alessandri + */ +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); +} diff --git a/predis/src/Command/ConnectionAuth.php b/predis/src/Command/ConnectionAuth.php new file mode 100644 index 0000000..c8c9ded --- /dev/null +++ b/predis/src/Command/ConnectionAuth.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/auth + * + * @author Daniele Alessandri + */ +class ConnectionAuth extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'AUTH'; + } +} diff --git a/predis/src/Command/ConnectionEcho.php b/predis/src/Command/ConnectionEcho.php new file mode 100644 index 0000000..fd49609 --- /dev/null +++ b/predis/src/Command/ConnectionEcho.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/echo + * + * @author Daniele Alessandri + */ +class ConnectionEcho extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ECHO'; + } +} diff --git a/predis/src/Command/ConnectionPing.php b/predis/src/Command/ConnectionPing.php new file mode 100644 index 0000000..fa9d734 --- /dev/null +++ b/predis/src/Command/ConnectionPing.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/ping + * + * @author Daniele Alessandri + */ +class ConnectionPing extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PING'; + } +} diff --git a/predis/src/Command/ConnectionQuit.php b/predis/src/Command/ConnectionQuit.php new file mode 100644 index 0000000..e59e31e --- /dev/null +++ b/predis/src/Command/ConnectionQuit.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/quit + * + * @author Daniele Alessandri + */ +class ConnectionQuit extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'QUIT'; + } +} diff --git a/predis/src/Command/ConnectionSelect.php b/predis/src/Command/ConnectionSelect.php new file mode 100644 index 0000000..1da8256 --- /dev/null +++ b/predis/src/Command/ConnectionSelect.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/select + * + * @author Daniele Alessandri + */ +class ConnectionSelect extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SELECT'; + } +} diff --git a/predis/src/Command/GeospatialGeoAdd.php b/predis/src/Command/GeospatialGeoAdd.php new file mode 100644 index 0000000..adca2ca --- /dev/null +++ b/predis/src/Command/GeospatialGeoAdd.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/geoadd + * + * @author Daniele Alessandri + */ +class GeospatialGeoAdd extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEOADD'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + foreach (array_pop($arguments) as $item) { + $arguments = array_merge($arguments, $item); + } + } + + return $arguments; + } +} diff --git a/predis/src/Command/GeospatialGeoDist.php b/predis/src/Command/GeospatialGeoDist.php new file mode 100644 index 0000000..17c5f54 --- /dev/null +++ b/predis/src/Command/GeospatialGeoDist.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/geodist + * + * @author Daniele Alessandri + */ +class GeospatialGeoDist extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEODIST'; + } +} diff --git a/predis/src/Command/GeospatialGeoHash.php b/predis/src/Command/GeospatialGeoHash.php new file mode 100644 index 0000000..2eccaf4 --- /dev/null +++ b/predis/src/Command/GeospatialGeoHash.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/geohash + * + * @author Daniele Alessandri + */ +class GeospatialGeoHash extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEOHASH'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + $members = array_pop($arguments); + $arguments = array_merge($arguments, $members); + } + + return $arguments; + } +} diff --git a/predis/src/Command/GeospatialGeoPos.php b/predis/src/Command/GeospatialGeoPos.php new file mode 100644 index 0000000..6b7a9a3 --- /dev/null +++ b/predis/src/Command/GeospatialGeoPos.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/geopos + * + * @author Daniele Alessandri + */ +class GeospatialGeoPos extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEOPOS'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + $members = array_pop($arguments); + $arguments = array_merge($arguments, $members); + } + + return $arguments; + } +} diff --git a/predis/src/Command/GeospatialGeoRadius.php b/predis/src/Command/GeospatialGeoRadius.php new file mode 100644 index 0000000..f205214 --- /dev/null +++ b/predis/src/Command/GeospatialGeoRadius.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/georadius + * + * @author Daniele Alessandri + */ +class GeospatialGeoRadius extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEORADIUS'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + if ($arguments && is_array(end($arguments))) { + $options = array_change_key_case(array_pop($arguments), CASE_UPPER); + + if (isset($options['WITHCOORD']) && $options['WITHCOORD'] == true) { + $arguments[] = 'WITHCOORD'; + } + + if (isset($options['WITHDIST']) && $options['WITHDIST'] == true) { + $arguments[] = 'WITHDIST'; + } + + if (isset($options['WITHHASH']) && $options['WITHHASH'] == true) { + $arguments[] = 'WITHHASH'; + } + + if (isset($options['COUNT'])) { + $arguments[] = 'COUNT'; + $arguments[] = $options['COUNT']; + } + + if (isset($options['SORT'])) { + $arguments[] = strtoupper($options['SORT']); + } + + if (isset($options['STORE'])) { + $arguments[] = 'STORE'; + $arguments[] = $options['STORE']; + } + + if (isset($options['STOREDIST'])) { + $arguments[] = 'STOREDIST'; + $arguments[] = $options['STOREDIST']; + } + } + + return $arguments; + } +} diff --git a/predis/src/Command/GeospatialGeoRadiusByMember.php b/predis/src/Command/GeospatialGeoRadiusByMember.php new file mode 100644 index 0000000..abfff7b --- /dev/null +++ b/predis/src/Command/GeospatialGeoRadiusByMember.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/georadiusbymember + * + * @author Daniele Alessandri + */ +class GeospatialGeoRadiusByMember extends GeospatialGeoRadius +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEORADIUSBYMEMBER'; + } +} diff --git a/predis/src/Command/HashDelete.php b/predis/src/Command/HashDelete.php new file mode 100644 index 0000000..d5d4c38 --- /dev/null +++ b/predis/src/Command/HashDelete.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hdel + * + * @author Daniele Alessandri + */ +class HashDelete extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HDEL'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeVariadic($arguments); + } +} diff --git a/predis/src/Command/HashExists.php b/predis/src/Command/HashExists.php new file mode 100644 index 0000000..ed8dc89 --- /dev/null +++ b/predis/src/Command/HashExists.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hexists + * + * @author Daniele Alessandri + */ +class HashExists extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HEXISTS'; + } +} diff --git a/predis/src/Command/HashGet.php b/predis/src/Command/HashGet.php new file mode 100644 index 0000000..20f33da --- /dev/null +++ b/predis/src/Command/HashGet.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hget + * + * @author Daniele Alessandri + */ +class HashGet extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HGET'; + } +} diff --git a/predis/src/Command/HashGetAll.php b/predis/src/Command/HashGetAll.php new file mode 100644 index 0000000..d698675 --- /dev/null +++ b/predis/src/Command/HashGetAll.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hgetall + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/HashGetMultiple.php b/predis/src/Command/HashGetMultiple.php new file mode 100644 index 0000000..820ce95 --- /dev/null +++ b/predis/src/Command/HashGetMultiple.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hmget + * + * @author Daniele Alessandri + */ +class HashGetMultiple extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HMGET'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeVariadic($arguments); + } +} diff --git a/predis/src/Command/HashIncrementBy.php b/predis/src/Command/HashIncrementBy.php new file mode 100644 index 0000000..a37359f --- /dev/null +++ b/predis/src/Command/HashIncrementBy.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hincrby + * + * @author Daniele Alessandri + */ +class HashIncrementBy extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HINCRBY'; + } +} diff --git a/predis/src/Command/HashIncrementByFloat.php b/predis/src/Command/HashIncrementByFloat.php new file mode 100644 index 0000000..bce9714 --- /dev/null +++ b/predis/src/Command/HashIncrementByFloat.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hincrbyfloat + * + * @author Daniele Alessandri + */ +class HashIncrementByFloat extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HINCRBYFLOAT'; + } +} diff --git a/predis/src/Command/HashKeys.php b/predis/src/Command/HashKeys.php new file mode 100644 index 0000000..2826602 --- /dev/null +++ b/predis/src/Command/HashKeys.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hkeys + * + * @author Daniele Alessandri + */ +class HashKeys extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HKEYS'; + } +} diff --git a/predis/src/Command/HashLength.php b/predis/src/Command/HashLength.php new file mode 100644 index 0000000..d70926f --- /dev/null +++ b/predis/src/Command/HashLength.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hlen + * + * @author Daniele Alessandri + */ +class HashLength extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HLEN'; + } +} diff --git a/predis/src/Command/HashScan.php b/predis/src/Command/HashScan.php new file mode 100644 index 0000000..afde74e --- /dev/null +++ b/predis/src/Command/HashScan.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hscan + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/HashSet.php b/predis/src/Command/HashSet.php new file mode 100644 index 0000000..cfff3c2 --- /dev/null +++ b/predis/src/Command/HashSet.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hset + * + * @author Daniele Alessandri + */ +class HashSet extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HSET'; + } +} diff --git a/predis/src/Command/HashSetMultiple.php b/predis/src/Command/HashSetMultiple.php new file mode 100644 index 0000000..6069e2a --- /dev/null +++ b/predis/src/Command/HashSetMultiple.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hmset + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/HashSetPreserve.php b/predis/src/Command/HashSetPreserve.php new file mode 100644 index 0000000..7a29116 --- /dev/null +++ b/predis/src/Command/HashSetPreserve.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hsetnx + * + * @author Daniele Alessandri + */ +class HashSetPreserve extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HSETNX'; + } +} diff --git a/predis/src/Command/HashStringLength.php b/predis/src/Command/HashStringLength.php new file mode 100644 index 0000000..7cfda80 --- /dev/null +++ b/predis/src/Command/HashStringLength.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hstrlen + * + * @author Daniele Alessandri + */ +class HashStringLength extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HSTRLEN'; + } +} diff --git a/predis/src/Command/HashValues.php b/predis/src/Command/HashValues.php new file mode 100644 index 0000000..0a5ea5f --- /dev/null +++ b/predis/src/Command/HashValues.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/hvals + * + * @author Daniele Alessandri + */ +class HashValues extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HVALS'; + } +} diff --git a/predis/src/Command/HyperLogLogAdd.php b/predis/src/Command/HyperLogLogAdd.php new file mode 100644 index 0000000..8fe49fc --- /dev/null +++ b/predis/src/Command/HyperLogLogAdd.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/pfadd + * + * @author Daniele Alessandri + */ +class HyperLogLogAdd extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PFADD'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeVariadic($arguments); + } +} diff --git a/predis/src/Command/HyperLogLogCount.php b/predis/src/Command/HyperLogLogCount.php new file mode 100644 index 0000000..0afe542 --- /dev/null +++ b/predis/src/Command/HyperLogLogCount.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/pfcount + * + * @author Daniele Alessandri + */ +class HyperLogLogCount extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PFCOUNT'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeArguments($arguments); + } +} diff --git a/predis/src/Command/HyperLogLogMerge.php b/predis/src/Command/HyperLogLogMerge.php new file mode 100644 index 0000000..c160be5 --- /dev/null +++ b/predis/src/Command/HyperLogLogMerge.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/pfmerge + * + * @author Daniele Alessandri + */ +class HyperLogLogMerge extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PFMERGE'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeArguments($arguments); + } +} diff --git a/predis/src/Command/KeyDelete.php b/predis/src/Command/KeyDelete.php new file mode 100644 index 0000000..89bdfdb --- /dev/null +++ b/predis/src/Command/KeyDelete.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/del + * + * @author Daniele Alessandri + */ +class KeyDelete extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DEL'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeArguments($arguments); + } +} diff --git a/predis/src/Command/KeyDump.php b/predis/src/Command/KeyDump.php new file mode 100644 index 0000000..6d9c488 --- /dev/null +++ b/predis/src/Command/KeyDump.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/dump + * + * @author Daniele Alessandri + */ +class KeyDump extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DUMP'; + } +} diff --git a/predis/src/Command/KeyExists.php b/predis/src/Command/KeyExists.php new file mode 100644 index 0000000..29e0648 --- /dev/null +++ b/predis/src/Command/KeyExists.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/exists + * + * @author Daniele Alessandri + */ +class KeyExists extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EXISTS'; + } +} diff --git a/predis/src/Command/KeyExpire.php b/predis/src/Command/KeyExpire.php new file mode 100644 index 0000000..66f4406 --- /dev/null +++ b/predis/src/Command/KeyExpire.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/expire + * + * @author Daniele Alessandri + */ +class KeyExpire extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EXPIRE'; + } +} diff --git a/predis/src/Command/KeyExpireAt.php b/predis/src/Command/KeyExpireAt.php new file mode 100644 index 0000000..0ae1b2d --- /dev/null +++ b/predis/src/Command/KeyExpireAt.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/expireat + * + * @author Daniele Alessandri + */ +class KeyExpireAt extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EXPIREAT'; + } +} diff --git a/predis/src/Command/KeyKeys.php b/predis/src/Command/KeyKeys.php new file mode 100644 index 0000000..6d74c40 --- /dev/null +++ b/predis/src/Command/KeyKeys.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/keys + * + * @author Daniele Alessandri + */ +class KeyKeys extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'KEYS'; + } +} diff --git a/predis/src/Command/KeyMigrate.php b/predis/src/Command/KeyMigrate.php new file mode 100644 index 0000000..3324ef9 --- /dev/null +++ b/predis/src/Command/KeyMigrate.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/migrate + * + * @author Daniele Alessandri + */ +class KeyMigrate extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MIGRATE'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + if (is_array(end($arguments))) { + foreach (array_pop($arguments) as $modifier => $value) { + $modifier = strtoupper($modifier); + + if ($modifier === 'COPY' && $value == true) { + $arguments[] = $modifier; + } + + if ($modifier === 'REPLACE' && $value == true) { + $arguments[] = $modifier; + } + } + } + + return $arguments; + } +} diff --git a/predis/src/Command/KeyMove.php b/predis/src/Command/KeyMove.php new file mode 100644 index 0000000..c849f08 --- /dev/null +++ b/predis/src/Command/KeyMove.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/move + * + * @author Daniele Alessandri + */ +class KeyMove extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MOVE'; + } +} diff --git a/predis/src/Command/KeyPersist.php b/predis/src/Command/KeyPersist.php new file mode 100644 index 0000000..f0cb679 --- /dev/null +++ b/predis/src/Command/KeyPersist.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/persist + * + * @author Daniele Alessandri + */ +class KeyPersist extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PERSIST'; + } +} diff --git a/predis/src/Command/KeyPreciseExpire.php b/predis/src/Command/KeyPreciseExpire.php new file mode 100644 index 0000000..258ec47 --- /dev/null +++ b/predis/src/Command/KeyPreciseExpire.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/pexpire + * + * @author Daniele Alessandri + */ +class KeyPreciseExpire extends KeyExpire +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PEXPIRE'; + } +} diff --git a/predis/src/Command/KeyPreciseExpireAt.php b/predis/src/Command/KeyPreciseExpireAt.php new file mode 100644 index 0000000..e419218 --- /dev/null +++ b/predis/src/Command/KeyPreciseExpireAt.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/pexpireat + * + * @author Daniele Alessandri + */ +class KeyPreciseExpireAt extends KeyExpireAt +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PEXPIREAT'; + } +} diff --git a/predis/src/Command/KeyPreciseTimeToLive.php b/predis/src/Command/KeyPreciseTimeToLive.php new file mode 100644 index 0000000..bdcd34b --- /dev/null +++ b/predis/src/Command/KeyPreciseTimeToLive.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/pttl + * + * @author Daniele Alessandri + */ +class KeyPreciseTimeToLive extends KeyTimeToLive +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PTTL'; + } +} diff --git a/predis/src/Command/KeyRandom.php b/predis/src/Command/KeyRandom.php new file mode 100644 index 0000000..b208b2d --- /dev/null +++ b/predis/src/Command/KeyRandom.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/randomkey + * + * @author Daniele Alessandri + */ +class KeyRandom extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RANDOMKEY'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + return $data !== '' ? $data : null; + } +} diff --git a/predis/src/Command/KeyRename.php b/predis/src/Command/KeyRename.php new file mode 100644 index 0000000..82e44fb --- /dev/null +++ b/predis/src/Command/KeyRename.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/rename + * + * @author Daniele Alessandri + */ +class KeyRename extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RENAME'; + } +} diff --git a/predis/src/Command/KeyRenamePreserve.php b/predis/src/Command/KeyRenamePreserve.php new file mode 100644 index 0000000..9793359 --- /dev/null +++ b/predis/src/Command/KeyRenamePreserve.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/renamenx + * + * @author Daniele Alessandri + */ +class KeyRenamePreserve extends KeyRename +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RENAMENX'; + } +} diff --git a/predis/src/Command/KeyRestore.php b/predis/src/Command/KeyRestore.php new file mode 100644 index 0000000..a5b0b2d --- /dev/null +++ b/predis/src/Command/KeyRestore.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/restore + * + * @author Daniele Alessandri + */ +class KeyRestore extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RESTORE'; + } +} diff --git a/predis/src/Command/KeyScan.php b/predis/src/Command/KeyScan.php new file mode 100644 index 0000000..05f5bb3 --- /dev/null +++ b/predis/src/Command/KeyScan.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/scan + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/KeySort.php b/predis/src/Command/KeySort.php new file mode 100644 index 0000000..fd449f1 --- /dev/null +++ b/predis/src/Command/KeySort.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sort + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/KeyTimeToLive.php b/predis/src/Command/KeyTimeToLive.php new file mode 100644 index 0000000..67697a6 --- /dev/null +++ b/predis/src/Command/KeyTimeToLive.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/ttl + * + * @author Daniele Alessandri + */ +class KeyTimeToLive extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'TTL'; + } +} diff --git a/predis/src/Command/KeyType.php b/predis/src/Command/KeyType.php new file mode 100644 index 0000000..f4f06e4 --- /dev/null +++ b/predis/src/Command/KeyType.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/type + * + * @author Daniele Alessandri + */ +class KeyType extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'TYPE'; + } +} diff --git a/predis/src/Command/ListIndex.php b/predis/src/Command/ListIndex.php new file mode 100644 index 0000000..27c64be --- /dev/null +++ b/predis/src/Command/ListIndex.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/lindex + * + * @author Daniele Alessandri + */ +class ListIndex extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LINDEX'; + } +} diff --git a/predis/src/Command/ListInsert.php b/predis/src/Command/ListInsert.php new file mode 100644 index 0000000..7d53d11 --- /dev/null +++ b/predis/src/Command/ListInsert.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/linsert + * + * @author Daniele Alessandri + */ +class ListInsert extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LINSERT'; + } +} diff --git a/predis/src/Command/ListLength.php b/predis/src/Command/ListLength.php new file mode 100644 index 0000000..6495beb --- /dev/null +++ b/predis/src/Command/ListLength.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/llen + * + * @author Daniele Alessandri + */ +class ListLength extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LLEN'; + } +} diff --git a/predis/src/Command/ListPopFirst.php b/predis/src/Command/ListPopFirst.php new file mode 100644 index 0000000..84d5d67 --- /dev/null +++ b/predis/src/Command/ListPopFirst.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/lpop + * + * @author Daniele Alessandri + */ +class ListPopFirst extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LPOP'; + } +} diff --git a/predis/src/Command/ListPopFirstBlocking.php b/predis/src/Command/ListPopFirstBlocking.php new file mode 100644 index 0000000..7dc7c00 --- /dev/null +++ b/predis/src/Command/ListPopFirstBlocking.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/blpop + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ListPopLast.php b/predis/src/Command/ListPopLast.php new file mode 100644 index 0000000..9e92db5 --- /dev/null +++ b/predis/src/Command/ListPopLast.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/rpop + * + * @author Daniele Alessandri + */ +class ListPopLast extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RPOP'; + } +} diff --git a/predis/src/Command/ListPopLastBlocking.php b/predis/src/Command/ListPopLastBlocking.php new file mode 100644 index 0000000..781eb91 --- /dev/null +++ b/predis/src/Command/ListPopLastBlocking.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/brpop + * + * @author Daniele Alessandri + */ +class ListPopLastBlocking extends ListPopFirstBlocking +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BRPOP'; + } +} diff --git a/predis/src/Command/ListPopLastPushHead.php b/predis/src/Command/ListPopLastPushHead.php new file mode 100644 index 0000000..f430eb2 --- /dev/null +++ b/predis/src/Command/ListPopLastPushHead.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/rpoplpush + * + * @author Daniele Alessandri + */ +class ListPopLastPushHead extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RPOPLPUSH'; + } +} diff --git a/predis/src/Command/ListPopLastPushHeadBlocking.php b/predis/src/Command/ListPopLastPushHeadBlocking.php new file mode 100644 index 0000000..ee9c93c --- /dev/null +++ b/predis/src/Command/ListPopLastPushHeadBlocking.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/brpoplpush + * + * @author Daniele Alessandri + */ +class ListPopLastPushHeadBlocking extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BRPOPLPUSH'; + } +} diff --git a/predis/src/Command/ListPushHead.php b/predis/src/Command/ListPushHead.php new file mode 100644 index 0000000..74bf7c4 --- /dev/null +++ b/predis/src/Command/ListPushHead.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/lpush + * + * @author Daniele Alessandri + */ +class ListPushHead extends ListPushTail +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LPUSH'; + } +} diff --git a/predis/src/Command/ListPushHeadX.php b/predis/src/Command/ListPushHeadX.php new file mode 100644 index 0000000..8e136b8 --- /dev/null +++ b/predis/src/Command/ListPushHeadX.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/lpushx + * + * @author Daniele Alessandri + */ +class ListPushHeadX extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LPUSHX'; + } +} diff --git a/predis/src/Command/ListPushTail.php b/predis/src/Command/ListPushTail.php new file mode 100644 index 0000000..f2a057c --- /dev/null +++ b/predis/src/Command/ListPushTail.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/rpush + * + * @author Daniele Alessandri + */ +class ListPushTail extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RPUSH'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeVariadic($arguments); + } +} diff --git a/predis/src/Command/ListPushTailX.php b/predis/src/Command/ListPushTailX.php new file mode 100644 index 0000000..1af3645 --- /dev/null +++ b/predis/src/Command/ListPushTailX.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/rpushx + * + * @author Daniele Alessandri + */ +class ListPushTailX extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RPUSHX'; + } +} diff --git a/predis/src/Command/ListRange.php b/predis/src/Command/ListRange.php new file mode 100644 index 0000000..32a21a6 --- /dev/null +++ b/predis/src/Command/ListRange.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/lrange + * + * @author Daniele Alessandri + */ +class ListRange extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LRANGE'; + } +} diff --git a/predis/src/Command/ListRemove.php b/predis/src/Command/ListRemove.php new file mode 100644 index 0000000..c580089 --- /dev/null +++ b/predis/src/Command/ListRemove.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/lrem + * + * @author Daniele Alessandri + */ +class ListRemove extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LREM'; + } +} diff --git a/predis/src/Command/ListSet.php b/predis/src/Command/ListSet.php new file mode 100644 index 0000000..5e59864 --- /dev/null +++ b/predis/src/Command/ListSet.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/lset + * + * @author Daniele Alessandri + */ +class ListSet extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LSET'; + } +} diff --git a/predis/src/Command/ListTrim.php b/predis/src/Command/ListTrim.php new file mode 100644 index 0000000..1931418 --- /dev/null +++ b/predis/src/Command/ListTrim.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/ltrim + * + * @author Daniele Alessandri + */ +class ListTrim extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LTRIM'; + } +} diff --git a/predis/src/Command/PrefixableCommandInterface.php b/predis/src/Command/PrefixableCommandInterface.php new file mode 100644 index 0000000..6d54554 --- /dev/null +++ b/predis/src/Command/PrefixableCommandInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * Defines a command whose keys can be prefixed. + * + * @author Daniele Alessandri + */ +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); +} diff --git a/predis/src/Command/Processor/KeyPrefixProcessor.php b/predis/src/Command/Processor/KeyPrefixProcessor.php new file mode 100644 index 0000000..7520127 --- /dev/null +++ b/predis/src/Command/Processor/KeyPrefixProcessor.php @@ -0,0 +1,450 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command\Processor; + +use Predis\Command\CommandInterface; +use Predis\Command\PrefixableCommandInterface; + +/** + * Command processor capable of prefixing keys stored in the arguments of Redis + * commands supported. + * + * @author Daniele Alessandri + */ +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' => 'static::all', + 'DEL' => 'static::all', + 'TYPE' => 'static::first', + 'KEYS' => 'static::first', + 'RENAME' => 'static::all', + 'RENAMENX' => 'static::all', + 'EXPIRE' => 'static::first', + 'EXPIREAT' => 'static::first', + 'TTL' => 'static::first', + 'MOVE' => 'static::first', + 'SORT' => 'static::sort', + 'DUMP' => 'static::first', + 'RESTORE' => 'static::first', + 'SET' => 'static::first', + 'SETNX' => 'static::first', + 'MSET' => 'static::interleaved', + 'MSETNX' => 'static::interleaved', + 'GET' => 'static::first', + 'MGET' => 'static::all', + 'GETSET' => 'static::first', + 'INCR' => 'static::first', + 'INCRBY' => 'static::first', + 'DECR' => 'static::first', + 'DECRBY' => 'static::first', + 'RPUSH' => 'static::first', + 'LPUSH' => 'static::first', + 'LLEN' => 'static::first', + 'LRANGE' => 'static::first', + 'LTRIM' => 'static::first', + 'LINDEX' => 'static::first', + 'LSET' => 'static::first', + 'LREM' => 'static::first', + 'LPOP' => 'static::first', + 'RPOP' => 'static::first', + 'RPOPLPUSH' => 'static::all', + 'SADD' => 'static::first', + 'SREM' => 'static::first', + 'SPOP' => 'static::first', + 'SMOVE' => 'static::skipLast', + 'SCARD' => 'static::first', + 'SISMEMBER' => 'static::first', + 'SINTER' => 'static::all', + 'SINTERSTORE' => 'static::all', + 'SUNION' => 'static::all', + 'SUNIONSTORE' => 'static::all', + 'SDIFF' => 'static::all', + 'SDIFFSTORE' => 'static::all', + 'SMEMBERS' => 'static::first', + 'SRANDMEMBER' => 'static::first', + 'ZADD' => 'static::first', + 'ZINCRBY' => 'static::first', + 'ZREM' => 'static::first', + 'ZRANGE' => 'static::first', + 'ZREVRANGE' => 'static::first', + 'ZRANGEBYSCORE' => 'static::first', + 'ZCARD' => 'static::first', + 'ZSCORE' => 'static::first', + 'ZREMRANGEBYSCORE' => 'static::first', + /* ---------------- Redis 2.0 ---------------- */ + 'SETEX' => 'static::first', + 'APPEND' => 'static::first', + 'SUBSTR' => 'static::first', + 'BLPOP' => 'static::skipLast', + 'BRPOP' => 'static::skipLast', + 'ZUNIONSTORE' => 'static::zsetStore', + 'ZINTERSTORE' => 'static::zsetStore', + 'ZCOUNT' => 'static::first', + 'ZRANK' => 'static::first', + 'ZREVRANK' => 'static::first', + 'ZREMRANGEBYRANK' => 'static::first', + 'HSET' => 'static::first', + 'HSETNX' => 'static::first', + 'HMSET' => 'static::first', + 'HINCRBY' => 'static::first', + 'HGET' => 'static::first', + 'HMGET' => 'static::first', + 'HDEL' => 'static::first', + 'HEXISTS' => 'static::first', + 'HLEN' => 'static::first', + 'HKEYS' => 'static::first', + 'HVALS' => 'static::first', + 'HGETALL' => 'static::first', + 'SUBSCRIBE' => 'static::all', + 'UNSUBSCRIBE' => 'static::all', + 'PSUBSCRIBE' => 'static::all', + 'PUNSUBSCRIBE' => 'static::all', + 'PUBLISH' => 'static::first', + /* ---------------- Redis 2.2 ---------------- */ + 'PERSIST' => 'static::first', + 'STRLEN' => 'static::first', + 'SETRANGE' => 'static::first', + 'GETRANGE' => 'static::first', + 'SETBIT' => 'static::first', + 'GETBIT' => 'static::first', + 'RPUSHX' => 'static::first', + 'LPUSHX' => 'static::first', + 'LINSERT' => 'static::first', + 'BRPOPLPUSH' => 'static::skipLast', + 'ZREVRANGEBYSCORE' => 'static::first', + 'WATCH' => 'static::all', + /* ---------------- Redis 2.6 ---------------- */ + 'PTTL' => 'static::first', + 'PEXPIRE' => 'static::first', + 'PEXPIREAT' => 'static::first', + 'PSETEX' => 'static::first', + 'INCRBYFLOAT' => 'static::first', + 'BITOP' => 'static::skipFirst', + 'BITCOUNT' => 'static::first', + 'HINCRBYFLOAT' => 'static::first', + 'EVAL' => 'static::evalKeys', + 'EVALSHA' => 'static::evalKeys', + 'MIGRATE' => 'static::migrate', + /* ---------------- Redis 2.8 ---------------- */ + 'SSCAN' => 'static::first', + 'ZSCAN' => 'static::first', + 'HSCAN' => 'static::first', + 'PFADD' => 'static::first', + 'PFCOUNT' => 'static::all', + 'PFMERGE' => 'static::all', + 'ZLEXCOUNT' => 'static::first', + 'ZRANGEBYLEX' => 'static::first', + 'ZREMRANGEBYLEX' => 'static::first', + 'ZREVRANGEBYLEX' => 'static::first', + 'BITPOS' => 'static::first', + /* ---------------- Redis 3.2 ---------------- */ + 'HSTRLEN' => 'static::first', + 'BITFIELD' => 'static::first', + 'GEOADD' => 'static::first', + 'GEOHASH' => 'static::first', + 'GEOPOS' => 'static::first', + 'GEODIST' => 'static::first', + 'GEORADIUS' => 'static::georadius', + 'GEORADIUSBYMEMBER' => 'static::georadius', + ); + } + + /** + * 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 (strtoupper($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); + } + } + + /** + * Applies the specified prefix to the key of a MIGRATE command. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function migrate(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $arguments[2] = "$prefix{$arguments[2]}"; + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix to the key of a GEORADIUS command. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function georadius(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $arguments[0] = "$prefix{$arguments[0]}"; + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; + + if (($count = count($arguments)) > $startIndex) { + for ($i = $startIndex; $i < $count; ++$i) { + switch (strtoupper($arguments[$i])) { + case 'STORE': + case 'STOREDIST': + $arguments[$i] = "$prefix{$arguments[++$i]}"; + break; + + } + } + } + + $command->setRawArguments($arguments); + } + } +} diff --git a/predis/src/Command/Processor/ProcessorChain.php b/predis/src/Command/Processor/ProcessorChain.php new file mode 100644 index 0000000..0a4768b --- /dev/null +++ b/predis/src/Command/Processor/ProcessorChain.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command\Processor; + +use Predis\Command\CommandInterface; + +/** + * Default implementation of a command processors chain. + * + * @author Daniele Alessandri + */ +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); + } +} diff --git a/predis/src/Command/Processor/ProcessorInterface.php b/predis/src/Command/Processor/ProcessorInterface.php new file mode 100644 index 0000000..2f91058 --- /dev/null +++ b/predis/src/Command/Processor/ProcessorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command\Processor; + +use Predis\Command\CommandInterface; + +/** + * A command processor processes Redis commands before they are sent to Redis. + * + * @author Daniele Alessandri + */ +interface ProcessorInterface +{ + /** + * Processes the given Redis command. + * + * @param CommandInterface $command Command instance. + */ + public function process(CommandInterface $command); +} diff --git a/predis/src/Command/PubSubPublish.php b/predis/src/Command/PubSubPublish.php new file mode 100644 index 0000000..55508f8 --- /dev/null +++ b/predis/src/Command/PubSubPublish.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/publish + * + * @author Daniele Alessandri + */ +class PubSubPublish extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PUBLISH'; + } +} diff --git a/predis/src/Command/PubSubPubsub.php b/predis/src/Command/PubSubPubsub.php new file mode 100644 index 0000000..8cf8129 --- /dev/null +++ b/predis/src/Command/PubSubPubsub.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/pubsub + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/PubSubSubscribe.php b/predis/src/Command/PubSubSubscribe.php new file mode 100644 index 0000000..e477b31 --- /dev/null +++ b/predis/src/Command/PubSubSubscribe.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/subscribe + * + * @author Daniele Alessandri + */ +class PubSubSubscribe extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SUBSCRIBE'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeArguments($arguments); + } +} diff --git a/predis/src/Command/PubSubSubscribeByPattern.php b/predis/src/Command/PubSubSubscribeByPattern.php new file mode 100644 index 0000000..0118280 --- /dev/null +++ b/predis/src/Command/PubSubSubscribeByPattern.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/psubscribe + * + * @author Daniele Alessandri + */ +class PubSubSubscribeByPattern extends PubSubSubscribe +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PSUBSCRIBE'; + } +} diff --git a/predis/src/Command/PubSubUnsubscribe.php b/predis/src/Command/PubSubUnsubscribe.php new file mode 100644 index 0000000..d57c3ac --- /dev/null +++ b/predis/src/Command/PubSubUnsubscribe.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/unsubscribe + * + * @author Daniele Alessandri + */ +class PubSubUnsubscribe extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'UNSUBSCRIBE'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeArguments($arguments); + } +} diff --git a/predis/src/Command/PubSubUnsubscribeByPattern.php b/predis/src/Command/PubSubUnsubscribeByPattern.php new file mode 100644 index 0000000..4d76508 --- /dev/null +++ b/predis/src/Command/PubSubUnsubscribeByPattern.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/punsubscribe + * + * @author Daniele Alessandri + */ +class PubSubUnsubscribeByPattern extends PubSubUnsubscribe +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PUNSUBSCRIBE'; + } +} diff --git a/predis/src/Command/RawCommand.php b/predis/src/Command/RawCommand.php new file mode 100644 index 0000000..2dd48ca --- /dev/null +++ b/predis/src/Command/RawCommand.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * 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 + */ +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; + } +} diff --git a/predis/src/Command/ScriptCommand.php b/predis/src/Command/ScriptCommand.php new file mode 100644 index 0000000..a30bc1d --- /dev/null +++ b/predis/src/Command/ScriptCommand.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * 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 + */ +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; + } +} diff --git a/predis/src/Command/ServerBackgroundRewriteAOF.php b/predis/src/Command/ServerBackgroundRewriteAOF.php new file mode 100644 index 0000000..c66a294 --- /dev/null +++ b/predis/src/Command/ServerBackgroundRewriteAOF.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/bgrewriteaof + * + * @author Daniele Alessandri + */ +class ServerBackgroundRewriteAOF extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BGREWRITEAOF'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + return $data == 'Background append only file rewriting started'; + } +} diff --git a/predis/src/Command/ServerBackgroundSave.php b/predis/src/Command/ServerBackgroundSave.php new file mode 100644 index 0000000..4bf67ef --- /dev/null +++ b/predis/src/Command/ServerBackgroundSave.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/bgsave + * + * @author Daniele Alessandri + */ +class ServerBackgroundSave extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BGSAVE'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + return $data === 'Background saving started' ? true : $data; + } +} diff --git a/predis/src/Command/ServerClient.php b/predis/src/Command/ServerClient.php new file mode 100644 index 0000000..d00ebbf --- /dev/null +++ b/predis/src/Command/ServerClient.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @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 + */ +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; + } +} diff --git a/predis/src/Command/ServerCommand.php b/predis/src/Command/ServerCommand.php new file mode 100644 index 0000000..e9b3393 --- /dev/null +++ b/predis/src/Command/ServerCommand.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/command + * + * @author Daniele Alessandri + */ +class ServerCommand extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'COMMAND'; + } +} diff --git a/predis/src/Command/ServerConfig.php b/predis/src/Command/ServerConfig.php new file mode 100644 index 0000000..81e497a --- /dev/null +++ b/predis/src/Command/ServerConfig.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\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 + */ +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; + } +} diff --git a/predis/src/Command/ServerDatabaseSize.php b/predis/src/Command/ServerDatabaseSize.php new file mode 100644 index 0000000..6bc8972 --- /dev/null +++ b/predis/src/Command/ServerDatabaseSize.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/dbsize + * + * @author Daniele Alessandri + */ +class ServerDatabaseSize extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DBSIZE'; + } +} diff --git a/predis/src/Command/ServerEval.php b/predis/src/Command/ServerEval.php new file mode 100644 index 0000000..f5eefd8 --- /dev/null +++ b/predis/src/Command/ServerEval.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/eval + * + * @author Daniele Alessandri + */ +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)); + } +} diff --git a/predis/src/Command/ServerEvalSHA.php b/predis/src/Command/ServerEvalSHA.php new file mode 100644 index 0000000..520a8e9 --- /dev/null +++ b/predis/src/Command/ServerEvalSHA.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/evalsha + * + * @author Daniele Alessandri + */ +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); + } +} diff --git a/predis/src/Command/ServerFlushAll.php b/predis/src/Command/ServerFlushAll.php new file mode 100644 index 0000000..c35b2ad --- /dev/null +++ b/predis/src/Command/ServerFlushAll.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/flushall + * + * @author Daniele Alessandri + */ +class ServerFlushAll extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'FLUSHALL'; + } +} diff --git a/predis/src/Command/ServerFlushDatabase.php b/predis/src/Command/ServerFlushDatabase.php new file mode 100644 index 0000000..3da6b32 --- /dev/null +++ b/predis/src/Command/ServerFlushDatabase.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/flushdb + * + * @author Daniele Alessandri + */ +class ServerFlushDatabase extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'FLUSHDB'; + } +} diff --git a/predis/src/Command/ServerInfo.php b/predis/src/Command/ServerInfo.php new file mode 100644 index 0000000..96d6ada --- /dev/null +++ b/predis/src/Command/ServerInfo.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/info + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ServerInfoV26x.php b/predis/src/Command/ServerInfoV26x.php new file mode 100644 index 0000000..90c9b71 --- /dev/null +++ b/predis/src/Command/ServerInfoV26x.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/info + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ServerLastSave.php b/predis/src/Command/ServerLastSave.php new file mode 100644 index 0000000..feeb19a --- /dev/null +++ b/predis/src/Command/ServerLastSave.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/lastsave + * + * @author Daniele Alessandri + */ +class ServerLastSave extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LASTSAVE'; + } +} diff --git a/predis/src/Command/ServerMonitor.php b/predis/src/Command/ServerMonitor.php new file mode 100644 index 0000000..1c3d330 --- /dev/null +++ b/predis/src/Command/ServerMonitor.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/monitor + * + * @author Daniele Alessandri + */ +class ServerMonitor extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MONITOR'; + } +} diff --git a/predis/src/Command/ServerObject.php b/predis/src/Command/ServerObject.php new file mode 100644 index 0000000..f921701 --- /dev/null +++ b/predis/src/Command/ServerObject.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/object + * + * @author Daniele Alessandri + */ +class ServerObject extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'OBJECT'; + } +} diff --git a/predis/src/Command/ServerSave.php b/predis/src/Command/ServerSave.php new file mode 100644 index 0000000..addefe2 --- /dev/null +++ b/predis/src/Command/ServerSave.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/save + * + * @author Daniele Alessandri + */ +class ServerSave extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SAVE'; + } +} diff --git a/predis/src/Command/ServerScript.php b/predis/src/Command/ServerScript.php new file mode 100644 index 0000000..7a01018 --- /dev/null +++ b/predis/src/Command/ServerScript.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/script + * + * @author Daniele Alessandri + */ +class ServerScript extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SCRIPT'; + } +} diff --git a/predis/src/Command/ServerSentinel.php b/predis/src/Command/ServerSentinel.php new file mode 100644 index 0000000..c0962db --- /dev/null +++ b/predis/src/Command/ServerSentinel.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/topics/sentinel + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ServerShutdown.php b/predis/src/Command/ServerShutdown.php new file mode 100644 index 0000000..f5b745a --- /dev/null +++ b/predis/src/Command/ServerShutdown.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/shutdown + * + * @author Daniele Alessandri + */ +class ServerShutdown extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SHUTDOWN'; + } +} diff --git a/predis/src/Command/ServerSlaveOf.php b/predis/src/Command/ServerSlaveOf.php new file mode 100644 index 0000000..4ff4455 --- /dev/null +++ b/predis/src/Command/ServerSlaveOf.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/slaveof + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ServerSlowlog.php b/predis/src/Command/ServerSlowlog.php new file mode 100644 index 0000000..137ff59 --- /dev/null +++ b/predis/src/Command/ServerSlowlog.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/slowlog + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ServerTime.php b/predis/src/Command/ServerTime.php new file mode 100644 index 0000000..589f92c --- /dev/null +++ b/predis/src/Command/ServerTime.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/time + * + * @author Daniele Alessandri + */ +class ServerTime extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'TIME'; + } +} diff --git a/predis/src/Command/SetAdd.php b/predis/src/Command/SetAdd.php new file mode 100644 index 0000000..c118818 --- /dev/null +++ b/predis/src/Command/SetAdd.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sadd + * + * @author Daniele Alessandri + */ +class SetAdd extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SADD'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeVariadic($arguments); + } +} diff --git a/predis/src/Command/SetCardinality.php b/predis/src/Command/SetCardinality.php new file mode 100644 index 0000000..a9f959b --- /dev/null +++ b/predis/src/Command/SetCardinality.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/scard + * + * @author Daniele Alessandri + */ +class SetCardinality extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SCARD'; + } +} diff --git a/predis/src/Command/SetDifference.php b/predis/src/Command/SetDifference.php new file mode 100644 index 0000000..35f23f9 --- /dev/null +++ b/predis/src/Command/SetDifference.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sdiff + * + * @author Daniele Alessandri + */ +class SetDifference extends SetIntersection +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SDIFF'; + } +} diff --git a/predis/src/Command/SetDifferenceStore.php b/predis/src/Command/SetDifferenceStore.php new file mode 100644 index 0000000..0cb7815 --- /dev/null +++ b/predis/src/Command/SetDifferenceStore.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sdiffstore + * + * @author Daniele Alessandri + */ +class SetDifferenceStore extends SetIntersectionStore +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SDIFFSTORE'; + } +} diff --git a/predis/src/Command/SetIntersection.php b/predis/src/Command/SetIntersection.php new file mode 100644 index 0000000..d18258f --- /dev/null +++ b/predis/src/Command/SetIntersection.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sinter + * + * @author Daniele Alessandri + */ +class SetIntersection extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SINTER'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeArguments($arguments); + } +} diff --git a/predis/src/Command/SetIntersectionStore.php b/predis/src/Command/SetIntersectionStore.php new file mode 100644 index 0000000..b748618 --- /dev/null +++ b/predis/src/Command/SetIntersectionStore.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sinterstore + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/SetIsMember.php b/predis/src/Command/SetIsMember.php new file mode 100644 index 0000000..7742522 --- /dev/null +++ b/predis/src/Command/SetIsMember.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sismember + * + * @author Daniele Alessandri + */ +class SetIsMember extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SISMEMBER'; + } +} diff --git a/predis/src/Command/SetMembers.php b/predis/src/Command/SetMembers.php new file mode 100644 index 0000000..f4076ae --- /dev/null +++ b/predis/src/Command/SetMembers.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/smembers + * + * @author Daniele Alessandri + */ +class SetMembers extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SMEMBERS'; + } +} diff --git a/predis/src/Command/SetMove.php b/predis/src/Command/SetMove.php new file mode 100644 index 0000000..edd4e51 --- /dev/null +++ b/predis/src/Command/SetMove.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/smove + * + * @author Daniele Alessandri + */ +class SetMove extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SMOVE'; + } +} diff --git a/predis/src/Command/SetPop.php b/predis/src/Command/SetPop.php new file mode 100644 index 0000000..b78d3f3 --- /dev/null +++ b/predis/src/Command/SetPop.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/spop + * + * @author Daniele Alessandri + */ +class SetPop extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SPOP'; + } +} diff --git a/predis/src/Command/SetRandomMember.php b/predis/src/Command/SetRandomMember.php new file mode 100644 index 0000000..2cb79a0 --- /dev/null +++ b/predis/src/Command/SetRandomMember.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/srandmember + * + * @author Daniele Alessandri + */ +class SetRandomMember extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SRANDMEMBER'; + } +} diff --git a/predis/src/Command/SetRemove.php b/predis/src/Command/SetRemove.php new file mode 100644 index 0000000..b34710c --- /dev/null +++ b/predis/src/Command/SetRemove.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/srem + * + * @author Daniele Alessandri + */ +class SetRemove extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SREM'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeVariadic($arguments); + } +} diff --git a/predis/src/Command/SetScan.php b/predis/src/Command/SetScan.php new file mode 100644 index 0000000..d42b28d --- /dev/null +++ b/predis/src/Command/SetScan.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sscan + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/SetUnion.php b/predis/src/Command/SetUnion.php new file mode 100644 index 0000000..7da842b --- /dev/null +++ b/predis/src/Command/SetUnion.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sunion + * + * @author Daniele Alessandri + */ +class SetUnion extends SetIntersection +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SUNION'; + } +} diff --git a/predis/src/Command/SetUnionStore.php b/predis/src/Command/SetUnionStore.php new file mode 100644 index 0000000..eac821a --- /dev/null +++ b/predis/src/Command/SetUnionStore.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/sunionstore + * + * @author Daniele Alessandri + */ +class SetUnionStore extends SetIntersectionStore +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SUNIONSTORE'; + } +} diff --git a/predis/src/Command/StringAppend.php b/predis/src/Command/StringAppend.php new file mode 100644 index 0000000..dac8b84 --- /dev/null +++ b/predis/src/Command/StringAppend.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/append + * + * @author Daniele Alessandri + */ +class StringAppend extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'APPEND'; + } +} diff --git a/predis/src/Command/StringBitCount.php b/predis/src/Command/StringBitCount.php new file mode 100644 index 0000000..193cce9 --- /dev/null +++ b/predis/src/Command/StringBitCount.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/bitcount + * + * @author Daniele Alessandri + */ +class StringBitCount extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITCOUNT'; + } +} diff --git a/predis/src/Command/StringBitField.php b/predis/src/Command/StringBitField.php new file mode 100644 index 0000000..9f4deaa --- /dev/null +++ b/predis/src/Command/StringBitField.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/bitfield + * + * @author Daniele Alessandri + */ +class StringBitField extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITFIELD'; + } +} diff --git a/predis/src/Command/StringBitOp.php b/predis/src/Command/StringBitOp.php new file mode 100644 index 0000000..e04ee79 --- /dev/null +++ b/predis/src/Command/StringBitOp.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/bitop + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/StringBitPos.php b/predis/src/Command/StringBitPos.php new file mode 100644 index 0000000..4295766 --- /dev/null +++ b/predis/src/Command/StringBitPos.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/bitpos + * + * @author Daniele Alessandri + */ +class StringBitPos extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITPOS'; + } +} diff --git a/predis/src/Command/StringDecrement.php b/predis/src/Command/StringDecrement.php new file mode 100644 index 0000000..aa5808c --- /dev/null +++ b/predis/src/Command/StringDecrement.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/decr + * + * @author Daniele Alessandri + */ +class StringDecrement extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DECR'; + } +} diff --git a/predis/src/Command/StringDecrementBy.php b/predis/src/Command/StringDecrementBy.php new file mode 100644 index 0000000..cbf3e11 --- /dev/null +++ b/predis/src/Command/StringDecrementBy.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/decrby + * + * @author Daniele Alessandri + */ +class StringDecrementBy extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DECRBY'; + } +} diff --git a/predis/src/Command/StringGet.php b/predis/src/Command/StringGet.php new file mode 100644 index 0000000..138e915 --- /dev/null +++ b/predis/src/Command/StringGet.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/get + * + * @author Daniele Alessandri + */ +class StringGet extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GET'; + } +} diff --git a/predis/src/Command/StringGetBit.php b/predis/src/Command/StringGetBit.php new file mode 100644 index 0000000..3c5b4f9 --- /dev/null +++ b/predis/src/Command/StringGetBit.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/getbit + * + * @author Daniele Alessandri + */ +class StringGetBit extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GETBIT'; + } +} diff --git a/predis/src/Command/StringGetMultiple.php b/predis/src/Command/StringGetMultiple.php new file mode 100644 index 0000000..e340f9c --- /dev/null +++ b/predis/src/Command/StringGetMultiple.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/mget + * + * @author Daniele Alessandri + */ +class StringGetMultiple extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MGET'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeArguments($arguments); + } +} diff --git a/predis/src/Command/StringGetRange.php b/predis/src/Command/StringGetRange.php new file mode 100644 index 0000000..bb10565 --- /dev/null +++ b/predis/src/Command/StringGetRange.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/getrange + * + * @author Daniele Alessandri + */ +class StringGetRange extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GETRANGE'; + } +} diff --git a/predis/src/Command/StringGetSet.php b/predis/src/Command/StringGetSet.php new file mode 100644 index 0000000..b68870d --- /dev/null +++ b/predis/src/Command/StringGetSet.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/getset + * + * @author Daniele Alessandri + */ +class StringGetSet extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GETSET'; + } +} diff --git a/predis/src/Command/StringIncrement.php b/predis/src/Command/StringIncrement.php new file mode 100644 index 0000000..fa1846e --- /dev/null +++ b/predis/src/Command/StringIncrement.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/incr + * + * @author Daniele Alessandri + */ +class StringIncrement extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'INCR'; + } +} diff --git a/predis/src/Command/StringIncrementBy.php b/predis/src/Command/StringIncrementBy.php new file mode 100644 index 0000000..9d8241a --- /dev/null +++ b/predis/src/Command/StringIncrementBy.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/incrby + * + * @author Daniele Alessandri + */ +class StringIncrementBy extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'INCRBY'; + } +} diff --git a/predis/src/Command/StringIncrementByFloat.php b/predis/src/Command/StringIncrementByFloat.php new file mode 100644 index 0000000..164a086 --- /dev/null +++ b/predis/src/Command/StringIncrementByFloat.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/incrbyfloat + * + * @author Daniele Alessandri + */ +class StringIncrementByFloat extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'INCRBYFLOAT'; + } +} diff --git a/predis/src/Command/StringPreciseSetExpire.php b/predis/src/Command/StringPreciseSetExpire.php new file mode 100644 index 0000000..2faa954 --- /dev/null +++ b/predis/src/Command/StringPreciseSetExpire.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/psetex + * + * @author Daniele Alessandri + */ +class StringPreciseSetExpire extends StringSetExpire +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PSETEX'; + } +} diff --git a/predis/src/Command/StringSet.php b/predis/src/Command/StringSet.php new file mode 100644 index 0000000..b146994 --- /dev/null +++ b/predis/src/Command/StringSet.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/set + * + * @author Daniele Alessandri + */ +class StringSet extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SET'; + } +} diff --git a/predis/src/Command/StringSetBit.php b/predis/src/Command/StringSetBit.php new file mode 100644 index 0000000..7933b6b --- /dev/null +++ b/predis/src/Command/StringSetBit.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/setbit + * + * @author Daniele Alessandri + */ +class StringSetBit extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SETBIT'; + } +} diff --git a/predis/src/Command/StringSetExpire.php b/predis/src/Command/StringSetExpire.php new file mode 100644 index 0000000..f088170 --- /dev/null +++ b/predis/src/Command/StringSetExpire.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/setex + * + * @author Daniele Alessandri + */ +class StringSetExpire extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SETEX'; + } +} diff --git a/predis/src/Command/StringSetMultiple.php b/predis/src/Command/StringSetMultiple.php new file mode 100644 index 0000000..a3c5324 --- /dev/null +++ b/predis/src/Command/StringSetMultiple.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/mset + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/StringSetMultiplePreserve.php b/predis/src/Command/StringSetMultiplePreserve.php new file mode 100644 index 0000000..b46a88c --- /dev/null +++ b/predis/src/Command/StringSetMultiplePreserve.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/msetnx + * + * @author Daniele Alessandri + */ +class StringSetMultiplePreserve extends StringSetMultiple +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MSETNX'; + } +} diff --git a/predis/src/Command/StringSetPreserve.php b/predis/src/Command/StringSetPreserve.php new file mode 100644 index 0000000..e89c974 --- /dev/null +++ b/predis/src/Command/StringSetPreserve.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/setnx + * + * @author Daniele Alessandri + */ +class StringSetPreserve extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SETNX'; + } +} diff --git a/predis/src/Command/StringSetRange.php b/predis/src/Command/StringSetRange.php new file mode 100644 index 0000000..4d9389f --- /dev/null +++ b/predis/src/Command/StringSetRange.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/setrange + * + * @author Daniele Alessandri + */ +class StringSetRange extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SETRANGE'; + } +} diff --git a/predis/src/Command/StringStrlen.php b/predis/src/Command/StringStrlen.php new file mode 100644 index 0000000..10f492f --- /dev/null +++ b/predis/src/Command/StringStrlen.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/strlen + * + * @author Daniele Alessandri + */ +class StringStrlen extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'STRLEN'; + } +} diff --git a/predis/src/Command/StringSubstr.php b/predis/src/Command/StringSubstr.php new file mode 100644 index 0000000..3aab7ad --- /dev/null +++ b/predis/src/Command/StringSubstr.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/substr + * + * @author Daniele Alessandri + */ +class StringSubstr extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SUBSTR'; + } +} diff --git a/predis/src/Command/TransactionDiscard.php b/predis/src/Command/TransactionDiscard.php new file mode 100644 index 0000000..44aca2b --- /dev/null +++ b/predis/src/Command/TransactionDiscard.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/discard + * + * @author Daniele Alessandri + */ +class TransactionDiscard extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DISCARD'; + } +} diff --git a/predis/src/Command/TransactionExec.php b/predis/src/Command/TransactionExec.php new file mode 100644 index 0000000..dbd81aa --- /dev/null +++ b/predis/src/Command/TransactionExec.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/exec + * + * @author Daniele Alessandri + */ +class TransactionExec extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EXEC'; + } +} diff --git a/predis/src/Command/TransactionMulti.php b/predis/src/Command/TransactionMulti.php new file mode 100644 index 0000000..673bf55 --- /dev/null +++ b/predis/src/Command/TransactionMulti.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/multi + * + * @author Daniele Alessandri + */ +class TransactionMulti extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MULTI'; + } +} diff --git a/predis/src/Command/TransactionUnwatch.php b/predis/src/Command/TransactionUnwatch.php new file mode 100644 index 0000000..7925554 --- /dev/null +++ b/predis/src/Command/TransactionUnwatch.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/unwatch + * + * @author Daniele Alessandri + */ +class TransactionUnwatch extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'UNWATCH'; + } +} diff --git a/predis/src/Command/TransactionWatch.php b/predis/src/Command/TransactionWatch.php new file mode 100644 index 0000000..d360780 --- /dev/null +++ b/predis/src/Command/TransactionWatch.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/watch + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ZSetAdd.php b/predis/src/Command/ZSetAdd.php new file mode 100644 index 0000000..55e4729 --- /dev/null +++ b/predis/src/Command/ZSetAdd.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zadd + * + * @author Daniele Alessandri + */ +class ZSetAdd extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZADD'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + if (is_array(end($arguments))) { + foreach (array_pop($arguments) as $member => $score) { + $arguments[] = $score; + $arguments[] = $member; + } + } + + return $arguments; + } +} diff --git a/predis/src/Command/ZSetCardinality.php b/predis/src/Command/ZSetCardinality.php new file mode 100644 index 0000000..1033200 --- /dev/null +++ b/predis/src/Command/ZSetCardinality.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zcard + * + * @author Daniele Alessandri + */ +class ZSetCardinality extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZCARD'; + } +} diff --git a/predis/src/Command/ZSetCount.php b/predis/src/Command/ZSetCount.php new file mode 100644 index 0000000..918bd2b --- /dev/null +++ b/predis/src/Command/ZSetCount.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zcount + * + * @author Daniele Alessandri + */ +class ZSetCount extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZCOUNT'; + } +} diff --git a/predis/src/Command/ZSetIncrementBy.php b/predis/src/Command/ZSetIncrementBy.php new file mode 100644 index 0000000..245a8e0 --- /dev/null +++ b/predis/src/Command/ZSetIncrementBy.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zincrby + * + * @author Daniele Alessandri + */ +class ZSetIncrementBy extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZINCRBY'; + } +} diff --git a/predis/src/Command/ZSetIntersectionStore.php b/predis/src/Command/ZSetIntersectionStore.php new file mode 100644 index 0000000..572a7a3 --- /dev/null +++ b/predis/src/Command/ZSetIntersectionStore.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zinterstore + * + * @author Daniele Alessandri + */ +class ZSetIntersectionStore extends ZSetUnionStore +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZINTERSTORE'; + } +} diff --git a/predis/src/Command/ZSetLexCount.php b/predis/src/Command/ZSetLexCount.php new file mode 100644 index 0000000..447b8eb --- /dev/null +++ b/predis/src/Command/ZSetLexCount.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zlexcount + * + * @author Daniele Alessandri + */ +class ZSetLexCount extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZLEXCOUNT'; + } +} diff --git a/predis/src/Command/ZSetRange.php b/predis/src/Command/ZSetRange.php new file mode 100644 index 0000000..ce72c7c --- /dev/null +++ b/predis/src/Command/ZSetRange.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrange + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ZSetRangeByLex.php b/predis/src/Command/ZSetRangeByLex.php new file mode 100644 index 0000000..9b2991a --- /dev/null +++ b/predis/src/Command/ZSetRangeByLex.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrangebylex + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ZSetRangeByScore.php b/predis/src/Command/ZSetRangeByScore.php new file mode 100644 index 0000000..961a5bc --- /dev/null +++ b/predis/src/Command/ZSetRangeByScore.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrangebyscore + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ZSetRank.php b/predis/src/Command/ZSetRank.php new file mode 100644 index 0000000..d0c9c53 --- /dev/null +++ b/predis/src/Command/ZSetRank.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrank + * + * @author Daniele Alessandri + */ +class ZSetRank extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZRANK'; + } +} diff --git a/predis/src/Command/ZSetRemove.php b/predis/src/Command/ZSetRemove.php new file mode 100644 index 0000000..cd8ada0 --- /dev/null +++ b/predis/src/Command/ZSetRemove.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrem + * + * @author Daniele Alessandri + */ +class ZSetRemove extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREM'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(array $arguments) + { + return self::normalizeVariadic($arguments); + } +} diff --git a/predis/src/Command/ZSetRemoveRangeByLex.php b/predis/src/Command/ZSetRemoveRangeByLex.php new file mode 100644 index 0000000..9ea2d9e --- /dev/null +++ b/predis/src/Command/ZSetRemoveRangeByLex.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zremrangebylex + * + * @author Daniele Alessandri + */ +class ZSetRemoveRangeByLex extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREMRANGEBYLEX'; + } +} diff --git a/predis/src/Command/ZSetRemoveRangeByRank.php b/predis/src/Command/ZSetRemoveRangeByRank.php new file mode 100644 index 0000000..89cd5ba --- /dev/null +++ b/predis/src/Command/ZSetRemoveRangeByRank.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zremrangebyrank + * + * @author Daniele Alessandri + */ +class ZSetRemoveRangeByRank extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREMRANGEBYRANK'; + } +} diff --git a/predis/src/Command/ZSetRemoveRangeByScore.php b/predis/src/Command/ZSetRemoveRangeByScore.php new file mode 100644 index 0000000..a7c3081 --- /dev/null +++ b/predis/src/Command/ZSetRemoveRangeByScore.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zremrangebyscore + * + * @author Daniele Alessandri + */ +class ZSetRemoveRangeByScore extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREMRANGEBYSCORE'; + } +} diff --git a/predis/src/Command/ZSetReverseRange.php b/predis/src/Command/ZSetReverseRange.php new file mode 100644 index 0000000..6a46a7a --- /dev/null +++ b/predis/src/Command/ZSetReverseRange.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrevrange + * + * @author Daniele Alessandri + */ +class ZSetReverseRange extends ZSetRange +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREVRANGE'; + } +} diff --git a/predis/src/Command/ZSetReverseRangeByLex.php b/predis/src/Command/ZSetReverseRangeByLex.php new file mode 100644 index 0000000..5dd611d --- /dev/null +++ b/predis/src/Command/ZSetReverseRangeByLex.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrevrangebylex + * + * @author Daniele Alessandri + */ +class ZSetReverseRangeByLex extends ZSetRangeByLex +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREVRANGEBYLEX'; + } +} diff --git a/predis/src/Command/ZSetReverseRangeByScore.php b/predis/src/Command/ZSetReverseRangeByScore.php new file mode 100644 index 0000000..1078eb7 --- /dev/null +++ b/predis/src/Command/ZSetReverseRangeByScore.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrevrangebyscore + * + * @author Daniele Alessandri + */ +class ZSetReverseRangeByScore extends ZSetRangeByScore +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREVRANGEBYSCORE'; + } +} diff --git a/predis/src/Command/ZSetReverseRank.php b/predis/src/Command/ZSetReverseRank.php new file mode 100644 index 0000000..33fb815 --- /dev/null +++ b/predis/src/Command/ZSetReverseRank.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrevrank + * + * @author Daniele Alessandri + */ +class ZSetReverseRank extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREVRANK'; + } +} diff --git a/predis/src/Command/ZSetScan.php b/predis/src/Command/ZSetScan.php new file mode 100644 index 0000000..1dc2352 --- /dev/null +++ b/predis/src/Command/ZSetScan.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zscan + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Command/ZSetScore.php b/predis/src/Command/ZSetScore.php new file mode 100644 index 0000000..2e7fce8 --- /dev/null +++ b/predis/src/Command/ZSetScore.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zscore + * + * @author Daniele Alessandri + */ +class ZSetScore extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZSCORE'; + } +} diff --git a/predis/src/Command/ZSetUnionStore.php b/predis/src/Command/ZSetUnionStore.php new file mode 100644 index 0000000..befc5ce --- /dev/null +++ b/predis/src/Command/ZSetUnionStore.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zunionstore + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/CommunicationException.php b/predis/src/CommunicationException.php new file mode 100644 index 0000000..13fe357 --- /dev/null +++ b/predis/src/CommunicationException.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis; + +use Predis\Connection\NodeConnectionInterface; + +/** + * Base exception class for network-related errors. + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Configuration/ClusterOption.php b/predis/src/Configuration/ClusterOption.php new file mode 100644 index 0000000..69e36de --- /dev/null +++ b/predis/src/Configuration/ClusterOption.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Configuration; + +use Predis\Connection\Aggregate\ClusterInterface; +use Predis\Connection\Aggregate\PredisCluster; +use Predis\Connection\Aggregate\RedisCluster; + +/** + * Configures an aggregate connection used for clustering + * multiple Redis nodes using various implementations with + * different algorithms or strategies. + * + * @author Daniele Alessandri + */ +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(); + } +} diff --git a/predis/src/Configuration/ConnectionFactoryOption.php b/predis/src/Configuration/ConnectionFactoryOption.php new file mode 100644 index 0000000..bf8479c --- /dev/null +++ b/predis/src/Configuration/ConnectionFactoryOption.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Configuration; + +use Predis\Connection\Factory; +use Predis\Connection\FactoryInterface; + +/** + * Configures a connection factory used by the client to create new connection + * instances for single Redis nodes. + * + * @author Daniele Alessandri + */ +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) + { + $factory = new Factory(); + + if ($options->defined('parameters')) { + $factory->setDefaultParameters($options->parameters); + } + + return $factory; + } +} diff --git a/predis/src/Configuration/ExceptionsOption.php b/predis/src/Configuration/ExceptionsOption.php new file mode 100644 index 0000000..337733e --- /dev/null +++ b/predis/src/Configuration/ExceptionsOption.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Configuration; + +/** + * 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 + */ +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; + } +} diff --git a/predis/src/Configuration/OptionInterface.php b/predis/src/Configuration/OptionInterface.php new file mode 100644 index 0000000..b31e0c9 --- /dev/null +++ b/predis/src/Configuration/OptionInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Configuration; + +/** + * Defines an handler used by Predis\Configuration\Options to filter, validate + * or return default values for a given option. + * + * @author Daniele Alessandri + */ +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); +} diff --git a/predis/src/Configuration/Options.php b/predis/src/Configuration/Options.php new file mode 100644 index 0000000..c17dd54 --- /dev/null +++ b/predis/src/Configuration/Options.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Configuration; + +/** + * Manages Predis options with filtering, conversion and lazy initialization of + * values using a mini-DI container approach. + * + * {@inheritdoc} + * + * @author Daniele Alessandri + */ +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 (is_object($value) && 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; + } +} diff --git a/predis/src/Configuration/OptionsInterface.php b/predis/src/Configuration/OptionsInterface.php new file mode 100644 index 0000000..f811647 --- /dev/null +++ b/predis/src/Configuration/OptionsInterface.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Configuration; + +/** + * 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 + */ +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); +} diff --git a/predis/src/Configuration/PrefixOption.php b/predis/src/Configuration/PrefixOption.php new file mode 100644 index 0000000..5827cdc --- /dev/null +++ b/predis/src/Configuration/PrefixOption.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Configuration; + +use Predis\Command\Processor\KeyPrefixProcessor; +use Predis\Command\Processor\ProcessorInterface; + +/** + * Configures a command processor that apply the specified prefix string to a + * series of Redis commands considered prefixable. + * + * @author Daniele Alessandri + */ +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 + } +} diff --git a/predis/src/Configuration/ProfileOption.php b/predis/src/Configuration/ProfileOption.php new file mode 100644 index 0000000..864936e --- /dev/null +++ b/predis/src/Configuration/ProfileOption.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Configuration; + +use Predis\Profile\Factory; +use Predis\Profile\ProfileInterface; +use Predis\Profile\RedisProfile; + +/** + * 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 + */ +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 = 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 = Factory::getDefault(); + $this->setProcessors($options, $profile); + + return $profile; + } +} diff --git a/predis/src/Configuration/ReplicationOption.php b/predis/src/Configuration/ReplicationOption.php new file mode 100644 index 0000000..1a34f71 --- /dev/null +++ b/predis/src/Configuration/ReplicationOption.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Configuration; + +use Predis\Connection\Aggregate\MasterSlaveReplication; +use Predis\Connection\Aggregate\ReplicationInterface; +use Predis\Connection\Aggregate\SentinelReplication; + +/** + * Configures an aggregate connection used for master/slave replication among + * multiple Redis nodes. + * + * @author Daniele Alessandri + */ +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 ($value === 'sentinel') { + return function ($sentinels, $options) { + return new SentinelReplication($options->service, $sentinels, $options->connections); + }; + } + + 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) + { + $replication = new MasterSlaveReplication(); + + if ($options->autodiscovery) { + $replication->setConnectionFactory($options->connections); + $replication->setAutoDiscovery(true); + } + + return $replication; + } +} diff --git a/predis/src/Connection/AbstractConnection.php b/predis/src/Connection/AbstractConnection.php new file mode 100644 index 0000000..fb86513 --- /dev/null +++ b/predis/src/Connection/AbstractConnection.php @@ -0,0 +1,226 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\CommandInterface; +use Predis\CommunicationException; +use Predis\Protocol\ProtocolException; + +/** + * Base class with the common logic used by connection classes to communicate + * with Redis. + * + * @author Daniele Alessandri + */ +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. + * + * @throws \InvalidArgumentException + * + * @return ParametersInterface + */ + abstract protected function assertParameters(ParametersInterface $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 that returns an exception message augmented with useful + * details from the connection parameters. + * + * @param string $message Error message. + * + * @return string + */ + private function createExceptionMessage($message) + { + $parameters = $this->parameters; + + if ($parameters->scheme === 'unix') { + return "$message [$parameters->scheme:$parameters->path]"; + } + + if (filter_var($parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return "$message [$parameters->scheme://[$parameters->host]:$parameters->port]"; + } + + return "$message [$parameters->scheme://$parameters->host:$parameters->port]"; + } + + /** + * 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, static::createExceptionMessage($message), $code) + ); + } + + /** + * Helper method to handle protocol errors. + * + * @param string $message Error message. + */ + protected function onProtocolError($message) + { + CommunicationException::handle( + new ProtocolException($this, static::createExceptionMessage($message)) + ); + } + + /** + * {@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'); + } +} diff --git a/predis/src/Connection/Aggregate/ClusterInterface.php b/predis/src/Connection/Aggregate/ClusterInterface.php new file mode 100644 index 0000000..af0f5aa --- /dev/null +++ b/predis/src/Connection/Aggregate/ClusterInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection\Aggregate; + +use Predis\Connection\AggregateConnectionInterface; + +/** + * Defines a cluster of Redis servers formed by aggregating multiple connection + * instances to single Redis nodes. + * + * @author Daniele Alessandri + */ +interface ClusterInterface extends AggregateConnectionInterface +{ +} diff --git a/predis/src/Connection/Aggregate/MasterSlaveReplication.php b/predis/src/Connection/Aggregate/MasterSlaveReplication.php new file mode 100644 index 0000000..238cf2c --- /dev/null +++ b/predis/src/Connection/Aggregate/MasterSlaveReplication.php @@ -0,0 +1,509 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection\Aggregate; + +use Predis\ClientException; +use Predis\Command\CommandInterface; +use Predis\Command\RawCommand; +use Predis\Connection\ConnectionException; +use Predis\Connection\FactoryInterface; +use Predis\Connection\NodeConnectionInterface; +use Predis\Replication\MissingMasterException; +use Predis\Replication\ReplicationStrategy; +use Predis\Response\ErrorInterface as ResponseErrorInterface; + +/** + * Aggregate connection handling replication of Redis nodes configured in a + * single master / multiple slaves setup. + * + * @author Daniele Alessandri + */ +class MasterSlaveReplication implements ReplicationInterface +{ + /** + * @var ReplicationStrategy + */ + protected $strategy; + + /** + * @var NodeConnectionInterface + */ + protected $master; + + /** + * @var NodeConnectionInterface[] + */ + protected $slaves = array(); + + /** + * @var NodeConnectionInterface + */ + protected $current; + + /** + * @var bool + */ + protected $autoDiscovery = false; + + /** + * @var FactoryInterface + */ + protected $connectionFactory; + + /** + * {@inheritdoc} + */ + public function __construct(ReplicationStrategy $strategy = null) + { + $this->strategy = $strategy ?: new ReplicationStrategy(); + } + + /** + * Configures the automatic discovery of the replication configuration on failure. + * + * @param bool $value Enable or disable auto discovery. + */ + public function setAutoDiscovery($value) + { + if (!$this->connectionFactory) { + throw new ClientException('Automatic discovery requires a connection factory'); + } + + $this->autoDiscovery = (bool) $value; + } + + /** + * Sets the connection factory used to create the connections by the auto + * discovery procedure. + * + * @param FactoryInterface $connectionFactory Connection factory instance. + */ + public function setConnectionFactory(FactoryInterface $connectionFactory) + { + $this->connectionFactory = $connectionFactory; + } + + /** + * 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 ?: "slave-$connection"] = $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) { + if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) { + $this->current = $slave; + } else { + $this->current = $this->getMasterOrDie(); + } + + return $this->current; + } + + if ($this->current === $master = $this->getMasterOrDie()) { + return $master; + } + + if (!$this->strategy->isReadOperation($command) || !$this->slaves) { + $this->current = $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; + } + + /** + * {@inheritdoc} + */ + public function switchTo($connection) + { + if (!$connection instanceof NodeConnectionInterface) { + $connection = $this->getConnectionById($connection); + } + + if (!$connection) { + throw new \InvalidArgumentException('Invalid connection or connection not found.'); + } + + if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) { + throw new \InvalidArgumentException('Invalid connection or connection not found.'); + } + + $this->current = $connection; + } + + /** + * Switches to the master server. + */ + public function switchToMaster() + { + $this->switchTo('master'); + } + + /** + * Switches to a random slave server. + */ + public function switchToSlave() + { + $connection = $this->pickSlave(); + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function getCurrent() + { + return $this->current; + } + + /** + * {@inheritdoc} + */ + public function getMaster() + { + return $this->master; + } + + /** + * Returns the connection associated to the master server. + * + * @return NodeConnectionInterface + */ + private function getMasterOrDie() + { + if (!$connection = $this->getMaster()) { + throw new MissingMasterException('No master server available for replication'); + } + + return $connection; + } + + /** + * {@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() + { + if ($this->slaves) { + 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) { + if (!$this->current = $this->pickSlave()) { + if (!$this->current = $this->getMaster()) { + throw new ClientException('No available connection for replication'); + } + } + } + + $this->current->connect(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + if ($this->master) { + $this->master->disconnect(); + } + + foreach ($this->slaves as $connection) { + $connection->disconnect(); + } + } + + /** + * Handles response from INFO. + * + * @param string $response + * + * @return array + */ + private function handleInfoResponse($response) + { + $info = array(); + + foreach (preg_split('/\r?\n/', $response) as $row) { + if (strpos($row, ':') === false) { + continue; + } + + list($k, $v) = explode(':', $row, 2); + $info[$k] = $v; + } + + return $info; + } + + /** + * Fetches the replication configuration from one of the servers. + */ + public function discover() + { + if (!$this->connectionFactory) { + throw new ClientException('Discovery requires a connection factory'); + } + + RETRY_FETCH: { + try { + if ($connection = $this->getMaster()) { + $this->discoverFromMaster($connection, $this->connectionFactory); + } elseif ($connection = $this->pickSlave()) { + $this->discoverFromSlave($connection, $this->connectionFactory); + } else { + throw new ClientException('No connection available for discovery'); + } + } catch (ConnectionException $exception) { + $this->remove($connection); + goto RETRY_FETCH; + } + } + } + + /** + * Discovers the replication configuration by contacting the master node. + * + * @param NodeConnectionInterface $connection Connection to the master node. + * @param FactoryInterface $connectionFactory Connection factory instance. + */ + protected function discoverFromMaster(NodeConnectionInterface $connection, FactoryInterface $connectionFactory) + { + $response = $connection->executeCommand(RawCommand::create('INFO', 'REPLICATION')); + $replication = $this->handleInfoResponse($response); + + if ($replication['role'] !== 'master') { + throw new ClientException("Role mismatch (expected master, got slave) [$connection]"); + } + + $this->slaves = array(); + + foreach ($replication as $k => $v) { + $parameters = null; + + if (strpos($k, 'slave') === 0 && preg_match('/ip=(?P.*),port=(?P\d+)/', $v, $parameters)) { + $slaveConnection = $connectionFactory->create(array( + 'host' => $parameters['host'], + 'port' => $parameters['port'], + )); + + $this->add($slaveConnection); + } + } + } + + /** + * Discovers the replication configuration by contacting one of the slaves. + * + * @param NodeConnectionInterface $connection Connection to one of the slaves. + * @param FactoryInterface $connectionFactory Connection factory instance. + */ + protected function discoverFromSlave(NodeConnectionInterface $connection, FactoryInterface $connectionFactory) + { + $response = $connection->executeCommand(RawCommand::create('INFO', 'REPLICATION')); + $replication = $this->handleInfoResponse($response); + + if ($replication['role'] !== 'slave') { + throw new ClientException("Role mismatch (expected slave, got master) [$connection]"); + } + + $masterConnection = $connectionFactory->create(array( + 'host' => $replication['master_host'], + 'port' => $replication['master_port'], + 'alias' => 'master', + )); + + $this->add($masterConnection); + + $this->discoverFromMaster($masterConnection, $connectionFactory); + } + + /** + * Retries the execution of a command upon slave failure. + * + * @param CommandInterface $command Command instance. + * @param string $method Actual method. + * + * @return mixed + */ + private function retryCommandOnFailure(CommandInterface $command, $method) + { + RETRY_COMMAND: { + try { + $connection = $this->getConnection($command); + $response = $connection->$method($command); + + if ($response instanceof ResponseErrorInterface && $response->getErrorType() === 'LOADING') { + throw new ConnectionException($connection, "Redis is loading the dataset in memory [$connection]"); + } + } catch (ConnectionException $exception) { + $connection = $exception->getConnection(); + $connection->disconnect(); + + if ($connection === $this->master && !$this->autoDiscovery) { + // Throw immediately when master connection is failing, even + // when the command represents a read-only operation, unless + // automatic discovery has been enabled. + throw $exception; + } else { + // Otherwise remove the failing slave and attempt to execute + // the command again on one of the remaining slaves... + $this->remove($connection); + } + + // ... that is, unless we have no more connections to use. + if (!$this->slaves && !$this->master) { + throw $exception; + } elseif ($this->autoDiscovery) { + $this->discover(); + } + + goto RETRY_COMMAND; + } catch (MissingMasterException $exception) { + if ($this->autoDiscovery) { + $this->discover(); + } else { + throw $exception; + } + + goto RETRY_COMMAND; + } + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function __sleep() + { + return array('master', 'slaves', 'strategy'); + } +} diff --git a/predis/src/Connection/Aggregate/PredisCluster.php b/predis/src/Connection/Aggregate/PredisCluster.php new file mode 100644 index 0000000..33f98bf --- /dev/null +++ b/predis/src/Connection/Aggregate/PredisCluster.php @@ -0,0 +1,235 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection\Aggregate; + +use Predis\Cluster\PredisStrategy; +use Predis\Cluster\StrategyInterface; +use Predis\Command\CommandInterface; +use Predis\Connection\NodeConnectionInterface; +use Predis\NotSupportedException; + +/** + * Abstraction for a cluster of aggregate connections to various Redis servers + * implementing client-side sharding based on pluggable distribution strategies. + * + * @author Daniele Alessandri + * + * @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; + } +} diff --git a/predis/src/Connection/Aggregate/RedisCluster.php b/predis/src/Connection/Aggregate/RedisCluster.php new file mode 100644 index 0000000..c749cc8 --- /dev/null +++ b/predis/src/Connection/Aggregate/RedisCluster.php @@ -0,0 +1,673 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection\Aggregate; + +use Predis\ClientException; +use Predis\Cluster\RedisStrategy as RedisClusterStrategy; +use Predis\Cluster\StrategyInterface; +use Predis\Command\CommandInterface; +use Predis\Command\RawCommand; +use Predis\Connection\ConnectionException; +use Predis\Connection\FactoryInterface; +use Predis\Connection\NodeConnectionInterface; +use Predis\NotSupportedException; +use Predis\Response\ErrorInterface as ErrorResponseInterface; + +/** + * 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 + */ +class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable +{ + private $useClusterSlots = true; + private $pool = array(); + private $slots = array(); + private $slotsMap; + private $strategy; + private $connections; + private $retryLimit = 5; + + /** + * @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(); + } + + /** + * Sets the maximum number of retries for commands upon server failure. + * + * -1 = unlimited retry attempts + * 0 = no retry attempts (fails immediatly) + * n = fail only after n retry attempts + * + * @param int $retry Number of retry attempts. + */ + public function setRetryLimit($retry) + { + $this->retryLimit = (int) $retry; + } + + /** + * {@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 + ); + + $this->slots = array_diff($this->slots, array($connection)); + + 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. + * + * @return array + */ + public function buildSlotsMap() + { + $this->slotsMap = array(); + + foreach ($this->pool as $connectionID => $connection) { + $parameters = $connection->getParameters(); + + if (!isset($parameters->slots)) { + continue; + } + + foreach (explode(',', $parameters->slots) as $slotRange) { + $slots = explode('-', $slotRange, 2); + + if (!isset($slots[1])) { + $slots[1] = $slots[0]; + } + + $this->setSlots($slots[0], $slots[1], $connectionID); + } + } + + return $this->slotsMap; + } + + /** + * Queries the specified node of the cluster to fetch the updated slots map. + * + * When the connection fails, this method tries to execute the same command + * on a different connection picked at random from the pool of known nodes, + * up until the retry limit is reached. + * + * @param NodeConnectionInterface $connection Connection to a node of the cluster. + * + * @return mixed + */ + private function queryClusterNodeForSlotsMap(NodeConnectionInterface $connection) + { + $retries = 0; + $command = RawCommand::create('CLUSTER', 'SLOTS'); + + RETRY_COMMAND: { + try { + $response = $connection->executeCommand($command); + } catch (ConnectionException $exception) { + $connection = $exception->getConnection(); + $connection->disconnect(); + + $this->remove($connection); + + if ($retries === $this->retryLimit) { + throw $exception; + } + + if (!$connection = $this->getRandomConnection()) { + throw new ClientException('No connections left in the pool for `CLUSTER SLOTS`'); + } + + ++$retries; + goto RETRY_COMMAND; + } + } + + return $response; + } + + /** + * 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(); + } + + $this->resetSlotsMap(); + + $response = $this->queryClusterNodeForSlotsMap($connection); + + 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; + } + + /** + * Resets the slots map cache. + */ + public function resetSlotsMap() + { + $this->slotsMap = array(); + } + + /** + * Returns the current slots map for the cluster. + * + * The order of the returned $slot => $server dictionary is not guaranteed. + * + * @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 (!$this->pool) { + throw new ClientException('No connections available in the pool'); + } + + 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) + { + $separator = strrpos($connectionID, ':'); + + return $this->connections->create(array( + 'host' => substr($connectionID, 0, $separator), + 'port' => substr($connectionID, $separator + 1), + )); + } + + /** + * {@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. + * + * @throws \OutOfBoundsException + * + * @return NodeConnectionInterface + */ + 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; + } + + /** + * Ensures that a command is executed one more time on connection failure. + * + * The connection to the node that generated the error is evicted from the + * pool before trying to fetch an updated slots map from another node. If + * the new slots map points to an unreachable server the client gives up and + * throws the exception as the nodes participating in the cluster may still + * have to agree that something changed in the configuration of the cluster. + * + * @param CommandInterface $command Command instance. + * @param string $method Actual method. + * + * @return mixed + */ + private function retryCommandOnFailure(CommandInterface $command, $method) + { + $failure = false; + + RETRY_COMMAND: { + try { + $response = $this->getConnection($command)->$method($command); + } catch (ConnectionException $exception) { + $connection = $exception->getConnection(); + $connection->disconnect(); + + $this->remove($connection); + + if ($failure) { + throw $exception; + } elseif ($this->useClusterSlots) { + $this->askSlotsMap(); + } + + $failure = true; + + goto RETRY_COMMAND; + } + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + $response = $this->retryCommandOnFailure($command, __FUNCTION__); + + if ($response instanceof ErrorResponseInterface) { + return $this->onErrorResponse($command, $response); + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function count() + { + return count($this->pool); + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + if ($this->useClusterSlots) { + $slotsmap = $this->getSlotsMap() ?: $this->askSlotsMap(); + } else { + $slotsmap = $this->getSlotsMap() ?: $this->buildSlotsMap(); + } + + $connections = array(); + + foreach (array_unique($slotsmap) as $node) { + if (!$connection = $this->getConnectionById($node)) { + $this->add($connection = $this->createConnection($node)); + } + + $connections[] = $connection; + } + + return new \ArrayIterator($connections); + } + + /** + * 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 enabled by default as + * 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; + } +} diff --git a/predis/src/Connection/Aggregate/ReplicationInterface.php b/predis/src/Connection/Aggregate/ReplicationInterface.php new file mode 100644 index 0000000..e09e826 --- /dev/null +++ b/predis/src/Connection/Aggregate/ReplicationInterface.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection\Aggregate; + +use Predis\Connection\AggregateConnectionInterface; +use Predis\Connection\NodeConnectionInterface; + +/** + * Defines a group of Redis nodes in a master / slave replication setup. + * + * @author Daniele Alessandri + */ +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(); +} diff --git a/predis/src/Connection/Aggregate/SentinelReplication.php b/predis/src/Connection/Aggregate/SentinelReplication.php new file mode 100644 index 0000000..5cae016 --- /dev/null +++ b/predis/src/Connection/Aggregate/SentinelReplication.php @@ -0,0 +1,720 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection\Aggregate; + +use Predis\Command\CommandInterface; +use Predis\Command\RawCommand; +use Predis\CommunicationException; +use Predis\Connection\ConnectionException; +use Predis\Connection\FactoryInterface as ConnectionFactoryInterface; +use Predis\Connection\NodeConnectionInterface; +use Predis\Connection\Parameters; +use Predis\Replication\ReplicationStrategy; +use Predis\Replication\RoleException; +use Predis\Response\ErrorInterface as ErrorResponseInterface; +use Predis\Response\ServerException; + +/** + * @author Daniele Alessandri + * @author Ville Mattila + */ +class SentinelReplication implements ReplicationInterface +{ + /** + * @var NodeConnectionInterface + */ + protected $master; + + /** + * @var NodeConnectionInterface[] + */ + protected $slaves = array(); + + /** + * @var NodeConnectionInterface + */ + protected $current; + + /** + * @var string + */ + protected $service; + + /** + * @var ConnectionFactoryInterface + */ + protected $connectionFactory; + + /** + * @var ReplicationStrategy + */ + protected $strategy; + + /** + * @var NodeConnectionInterface[] + */ + protected $sentinels = array(); + + /** + * @var NodeConnectionInterface + */ + protected $sentinelConnection; + + /** + * @var float + */ + protected $sentinelTimeout = 0.100; + + /** + * Max number of automatic retries of commands upon server failure. + * + * -1 = unlimited retry attempts + * 0 = no retry attempts (fails immediatly) + * n = fail only after n retry attempts + * + * @var int + */ + protected $retryLimit = 20; + + /** + * Time to wait in milliseconds before fetching a new configuration from one + * of the sentinel servers. + * + * @var int + */ + protected $retryWait = 1000; + + /** + * Flag for automatic fetching of available sentinels. + * + * @var bool + */ + protected $updateSentinels = false; + + /** + * @param string $service Name of the service for autodiscovery. + * @param array $sentinels Sentinel servers connection parameters. + * @param ConnectionFactoryInterface $connectionFactory Connection factory instance. + * @param ReplicationStrategy $strategy Replication strategy instance. + */ + public function __construct( + $service, + array $sentinels, + ConnectionFactoryInterface $connectionFactory, + ReplicationStrategy $strategy = null + ) { + $this->sentinels = $sentinels; + $this->service = $service; + $this->connectionFactory = $connectionFactory; + $this->strategy = $strategy ?: new ReplicationStrategy(); + } + + /** + * Sets a default timeout for connections to sentinels. + * + * When "timeout" is present in the connection parameters of sentinels, its + * value overrides the default sentinel timeout. + * + * @param float $timeout Timeout value. + */ + public function setSentinelTimeout($timeout) + { + $this->sentinelTimeout = (float) $timeout; + } + + /** + * Sets the maximum number of retries for commands upon server failure. + * + * -1 = unlimited retry attempts + * 0 = no retry attempts (fails immediatly) + * n = fail only after n retry attempts + * + * @param int $retry Number of retry attempts. + */ + public function setRetryLimit($retry) + { + $this->retryLimit = (int) $retry; + } + + /** + * Sets the time to wait (in seconds) before fetching a new configuration + * from one of the sentinels. + * + * @param float $seconds Time to wait before the next attempt. + */ + public function setRetryWait($seconds) + { + $this->retryWait = (float) $seconds; + } + + /** + * Set automatic fetching of available sentinels. + * + * @param bool $update Enable or disable automatic updates. + */ + public function setUpdateSentinels($update) + { + $this->updateSentinels = (bool) $update; + } + + /** + * Resets the current connection. + */ + protected function reset() + { + $this->current = null; + } + + /** + * Wipes the current list of master and slaves nodes. + */ + protected function wipeServerList() + { + $this->reset(); + + $this->master = null; + $this->slaves = array(); + } + + /** + * {@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 === $this->master) { + $this->master = null; + $this->reset(); + + return true; + } + + if (false !== $id = array_search($connection, $this->slaves, true)) { + unset($this->slaves[$id]); + $this->reset(); + + return true; + } + + return false; + } + + /** + * Creates a new connection to a sentinel server. + * + * @return NodeConnectionInterface + */ + protected function createSentinelConnection($parameters) + { + if ($parameters instanceof NodeConnectionInterface) { + return $parameters; + } + + if (is_string($parameters)) { + $parameters = Parameters::parse($parameters); + } + + if (is_array($parameters)) { + // We explicitly set "database" and "password" to null, + // so that no AUTH and SELECT command is send to the sentinels. + $parameters['database'] = null; + $parameters['password'] = null; + + if (!isset($parameters['timeout'])) { + $parameters['timeout'] = $this->sentinelTimeout; + } + } + + $connection = $this->connectionFactory->create($parameters); + + return $connection; + } + + /** + * Returns the current sentinel connection. + * + * If there is no active sentinel connection, a new connection is created. + * + * @return NodeConnectionInterface + */ + public function getSentinelConnection() + { + if (!$this->sentinelConnection) { + if (!$this->sentinels) { + throw new \Predis\ClientException('No sentinel server available for autodiscovery.'); + } + + $sentinel = array_shift($this->sentinels); + $this->sentinelConnection = $this->createSentinelConnection($sentinel); + } + + return $this->sentinelConnection; + } + + /** + * Fetches an updated list of sentinels from a sentinel. + */ + public function updateSentinels() + { + SENTINEL_QUERY: { + $sentinel = $this->getSentinelConnection(); + + try { + $payload = $sentinel->executeCommand( + RawCommand::create('SENTINEL', 'sentinels', $this->service) + ); + + $this->sentinels = array(); + // NOTE: sentinel server does not return itself, so we add it back. + $this->sentinels[] = $sentinel->getParameters()->toArray(); + + foreach ($payload as $sentinel) { + $this->sentinels[] = array( + 'host' => $sentinel[3], + 'port' => $sentinel[5], + ); + } + } catch (ConnectionException $exception) { + $this->sentinelConnection = null; + + goto SENTINEL_QUERY; + } + } + } + + /** + * Fetches the details for the master and slave servers from a sentinel. + */ + public function querySentinel() + { + $this->wipeServerList(); + + $this->updateSentinels(); + $this->getMaster(); + $this->getSlaves(); + } + + /** + * Handles error responses returned by redis-sentinel. + * + * @param NodeConnectionInterface $sentinel Connection to a sentinel server. + * @param ErrorResponseInterface $error Error response. + */ + private function handleSentinelErrorResponse(NodeConnectionInterface $sentinel, ErrorResponseInterface $error) + { + if ($error->getErrorType() === 'IDONTKNOW') { + throw new ConnectionException($sentinel, $error->getMessage()); + } else { + throw new ServerException($error->getMessage()); + } + } + + /** + * Fetches the details for the master server from a sentinel. + * + * @param NodeConnectionInterface $sentinel Connection to a sentinel server. + * @param string $service Name of the service. + * + * @return array + */ + protected function querySentinelForMaster(NodeConnectionInterface $sentinel, $service) + { + $payload = $sentinel->executeCommand( + RawCommand::create('SENTINEL', 'get-master-addr-by-name', $service) + ); + + if ($payload === null) { + throw new ServerException('ERR No such master with that name'); + } + + if ($payload instanceof ErrorResponseInterface) { + $this->handleSentinelErrorResponse($sentinel, $payload); + } + + return array( + 'host' => $payload[0], + 'port' => $payload[1], + 'alias' => 'master', + ); + } + + /** + * Fetches the details for the slave servers from a sentinel. + * + * @param NodeConnectionInterface $sentinel Connection to a sentinel server. + * @param string $service Name of the service. + * + * @return array + */ + protected function querySentinelForSlaves(NodeConnectionInterface $sentinel, $service) + { + $slaves = array(); + + $payload = $sentinel->executeCommand( + RawCommand::create('SENTINEL', 'slaves', $service) + ); + + if ($payload instanceof ErrorResponseInterface) { + $this->handleSentinelErrorResponse($sentinel, $payload); + } + + foreach ($payload as $slave) { + $flags = explode(',', $slave[9]); + + if (array_intersect($flags, array('s_down', 'o_down', 'disconnected'))) { + continue; + } + + $slaves[] = array( + 'host' => $slave[3], + 'port' => $slave[5], + 'alias' => "slave-$slave[1]", + ); + } + + return $slaves; + } + + /** + * {@inheritdoc} + */ + public function getCurrent() + { + return $this->current; + } + + /** + * {@inheritdoc} + */ + public function getMaster() + { + if ($this->master) { + return $this->master; + } + + if ($this->updateSentinels) { + $this->updateSentinels(); + } + + SENTINEL_QUERY: { + $sentinel = $this->getSentinelConnection(); + + try { + $masterParameters = $this->querySentinelForMaster($sentinel, $this->service); + $masterConnection = $this->connectionFactory->create($masterParameters); + + $this->add($masterConnection); + } catch (ConnectionException $exception) { + $this->sentinelConnection = null; + + goto SENTINEL_QUERY; + } + } + + return $masterConnection; + } + + /** + * {@inheritdoc} + */ + public function getSlaves() + { + if ($this->slaves) { + return array_values($this->slaves); + } + + if ($this->updateSentinels) { + $this->updateSentinels(); + } + + SENTINEL_QUERY: { + $sentinel = $this->getSentinelConnection(); + + try { + $slavesParameters = $this->querySentinelForSlaves($sentinel, $this->service); + + foreach ($slavesParameters as $slaveParameters) { + $this->add($this->connectionFactory->create($slaveParameters)); + } + } catch (ConnectionException $exception) { + $this->sentinelConnection = null; + + goto SENTINEL_QUERY; + } + } + + return array_values($this->slaves ?: array()); + } + + /** + * Returns a random slave. + * + * @return NodeConnectionInterface + */ + protected function pickSlave() + { + if ($slaves = $this->getSlaves()) { + return $slaves[rand(1, count($slaves)) - 1]; + } + } + + /** + * Returns the connection instance in charge for the given command. + * + * @param CommandInterface $command Command instance. + * + * @return NodeConnectionInterface + */ + private function getConnectionInternal(CommandInterface $command) + { + if (!$this->current) { + if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) { + $this->current = $slave; + } else { + $this->current = $this->getMaster(); + } + + return $this->current; + } + + if ($this->current === $this->master) { + return $this->current; + } + + if (!$this->strategy->isReadOperation($command)) { + $this->current = $this->getMaster(); + } + + return $this->current; + } + + /** + * Asserts that the specified connection matches an expected role. + * + * @param NodeConnectionInterface $sentinel Connection to a redis server. + * @param string $role Expected role of the server ("master", "slave" or "sentinel"). + */ + protected function assertConnectionRole(NodeConnectionInterface $connection, $role) + { + $role = strtolower($role); + $actualRole = $connection->executeCommand(RawCommand::create('ROLE')); + + if ($role !== $actualRole[0]) { + throw new RoleException($connection, "Expected $role but got $actualRole[0] [$connection]"); + } + } + + /** + * {@inheritdoc} + */ + public function getConnection(CommandInterface $command) + { + $connection = $this->getConnectionInternal($command); + + if (!$connection->isConnected()) { + // When we do not have any available slave in the pool we can expect + // read-only operations to hit the master server. + $expectedRole = $this->strategy->isReadOperation($command) && $this->slaves ? 'slave' : 'master'; + $this->assertConnectionRole($connection, $expectedRole); + } + + return $connection; + } + + /** + * {@inheritdoc} + */ + public function getConnectionById($connectionId) + { + if ($connectionId === 'master') { + return $this->getMaster(); + } + + $this->getSlaves(); + + if (isset($this->slaves[$connectionId])) { + return $this->slaves[$connectionId]; + } + } + + /** + * {@inheritdoc} + */ + public function switchTo($connection) + { + if (!$connection instanceof NodeConnectionInterface) { + $connection = $this->getConnectionById($connection); + } + + if ($connection && $connection === $this->current) { + return; + } + + if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) { + throw new \InvalidArgumentException('Invalid connection or connection not found.'); + } + + $connection->connect(); + + if ($this->current) { + $this->current->disconnect(); + } + + $this->current = $connection; + } + + /** + * Switches to the master server. + */ + public function switchToMaster() + { + $this->switchTo('master'); + } + + /** + * Switches to a random slave server. + */ + public function switchToSlave() + { + $connection = $this->pickSlave(); + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return $this->current ? $this->current->isConnected() : false; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if (!$this->current) { + if (!$this->current = $this->pickSlave()) { + $this->current = $this->getMaster(); + } + } + + $this->current->connect(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + if ($this->master) { + $this->master->disconnect(); + } + + foreach ($this->slaves as $connection) { + $connection->disconnect(); + } + } + + /** + * Retries the execution of a command upon server failure after asking a new + * configuration to one of the sentinels. + * + * @param CommandInterface $command Command instance. + * @param string $method Actual method. + * + * @return mixed + */ + private function retryCommandOnFailure(CommandInterface $command, $method) + { + $retries = 0; + + SENTINEL_RETRY: { + try { + $response = $this->getConnection($command)->$method($command); + } catch (CommunicationException $exception) { + $this->wipeServerList(); + $exception->getConnection()->disconnect(); + + if ($retries == $this->retryLimit) { + throw $exception; + } + + usleep($this->retryWait * 1000); + + ++$retries; + goto SENTINEL_RETRY; + } + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * Returns the underlying replication strategy. + * + * @return ReplicationStrategy + */ + public function getReplicationStrategy() + { + return $this->strategy; + } + + /** + * {@inheritdoc} + */ + public function __sleep() + { + return array( + 'master', 'slaves', 'service', 'sentinels', 'connectionFactory', 'strategy', + ); + } +} diff --git a/predis/src/Connection/AggregateConnectionInterface.php b/predis/src/Connection/AggregateConnectionInterface.php new file mode 100644 index 0000000..7eeaede --- /dev/null +++ b/predis/src/Connection/AggregateConnectionInterface.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\CommandInterface; + +/** + * Defines a virtual connection composed of multiple connection instances to + * single Redis nodes. + * + * @author Daniele Alessandri + */ +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); +} diff --git a/predis/src/Connection/CompositeConnectionInterface.php b/predis/src/Connection/CompositeConnectionInterface.php new file mode 100644 index 0000000..286e082 --- /dev/null +++ b/predis/src/Connection/CompositeConnectionInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +/** + * Defines a connection to communicate with a single Redis server that leverages + * an external protocol processor to handle pluggable protocol handlers. + * + * @author Daniele Alessandri + */ +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(); +} diff --git a/predis/src/Connection/CompositeStreamConnection.php b/predis/src/Connection/CompositeStreamConnection.php new file mode 100644 index 0000000..7a35340 --- /dev/null +++ b/predis/src/Connection/CompositeStreamConnection.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\CommandInterface; +use Predis\Protocol\ProtocolProcessorInterface; +use Predis\Protocol\Text\ProtocolProcessor as TextProtocolProcessor; + +/** + * 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 + */ +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')); + } +} diff --git a/predis/src/Connection/ConnectionException.php b/predis/src/Connection/ConnectionException.php new file mode 100644 index 0000000..ef2e9d7 --- /dev/null +++ b/predis/src/Connection/ConnectionException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\CommunicationException; + +/** + * Exception class that identifies connection-related errors. + * + * @author Daniele Alessandri + */ +class ConnectionException extends CommunicationException +{ +} diff --git a/predis/src/Connection/ConnectionInterface.php b/predis/src/Connection/ConnectionInterface.php new file mode 100644 index 0000000..11ace1b --- /dev/null +++ b/predis/src/Connection/ConnectionInterface.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\CommandInterface; + +/** + * Defines a connection object used to communicate with one or multiple + * Redis servers. + * + * @author Daniele Alessandri + */ +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); +} diff --git a/predis/src/Connection/Factory.php b/predis/src/Connection/Factory.php new file mode 100644 index 0000000..9c7272d --- /dev/null +++ b/predis/src/Connection/Factory.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\RawCommand; + +/** + * Standard connection factory for creating connections to Redis nodes. + * + * @author Daniele Alessandri + */ +class Factory implements FactoryInterface +{ + private $defaults = array(); + + protected $schemes = array( + 'tcp' => 'Predis\Connection\StreamConnection', + 'unix' => 'Predis\Connection\StreamConnection', + 'tls' => 'Predis\Connection\StreamConnection', + 'redis' => 'Predis\Connection\StreamConnection', + 'rediss' => '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. + * + * @throws \InvalidArgumentException + * + * @return mixed + */ + 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)); + } + } + + /** + * Assigns a default set of parameters applied to new connections. + * + * The set of parameters passed to create a new connection have precedence + * over the default values set for the connection factory. + * + * @param array $parameters Set of connection parameters. + */ + public function setDefaultParameters(array $parameters) + { + $this->defaults = $parameters; + } + + /** + * Returns the default set of parameters applied to new connections. + * + * @return array + */ + public function getDefaultParameters() + { + return $this->defaults; + } + + /** + * Creates a connection parameters instance from the supplied argument. + * + * @param mixed $parameters Original connection parameters. + * + * @return ParametersInterface + */ + protected function createParameters($parameters) + { + if (is_string($parameters)) { + $parameters = Parameters::parse($parameters); + } else { + $parameters = $parameters ?: array(); + } + + if ($this->defaults) { + $parameters += $this->defaults; + } + + return new Parameters($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)) + ); + } + } +} diff --git a/predis/src/Connection/FactoryInterface.php b/predis/src/Connection/FactoryInterface.php new file mode 100644 index 0000000..2bae083 --- /dev/null +++ b/predis/src/Connection/FactoryInterface.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +/** + * Interface for classes providing a factory of connections to Redis nodes. + * + * @author Daniele Alessandri + */ +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); +} diff --git a/predis/src/Connection/NodeConnectionInterface.php b/predis/src/Connection/NodeConnectionInterface.php new file mode 100644 index 0000000..665b862 --- /dev/null +++ b/predis/src/Connection/NodeConnectionInterface.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\CommandInterface; + +/** + * Defines a connection used to communicate with a single Redis node. + * + * @author Daniele Alessandri + */ +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(); +} diff --git a/predis/src/Connection/Parameters.php b/predis/src/Connection/Parameters.php new file mode 100644 index 0000000..3349c96 --- /dev/null +++ b/predis/src/Connection/Parameters.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +/** + * Container for connection parameters used to initialize connections to Redis. + * + * {@inheritdoc} + * + * @author Daniele Alessandri + */ +class Parameters implements ParametersInterface +{ + private $parameters; + + private static $defaults = array( + 'scheme' => 'tcp', + 'host' => '127.0.0.1', + 'port' => 6379, + ); + + /** + * @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. + * + * When using the "redis" and "rediss" schemes the URI is parsed according + * to the rules defined by the provisional registration documents approved + * by IANA. If the URI has a password in its "user-information" part or a + * database number in the "path" part these values override the values of + * "password" and "database" if they are present in the "query" part. + * + * @link http://www.iana.org/assignments/uri-schemes/prov/redis + * @link http://www.iana.org/assignments/uri-schemes/prov/rediss + * + * @param string $uri URI string. + * + * @throws \InvalidArgumentException + * + * @return array + */ + public static function parse($uri) + { + if (stripos($uri, 'unix://') === 0) { + // parse_url() can parse unix:/path/to/sock so we do not need the + // unix:///path/to/sock hack, we will support it anyway until 2.0. + $uri = str_ireplace('unix://', 'unix:', $uri); + } + + if (!$parsed = parse_url($uri)) { + throw new \InvalidArgumentException("Invalid parameters URI: $uri"); + } + + if ( + isset($parsed['host']) + && false !== strpos($parsed['host'], '[') + && false !== strpos($parsed['host'], ']') + ) { + $parsed['host'] = substr($parsed['host'], 1, -1); + } + + if (isset($parsed['query'])) { + parse_str($parsed['query'], $queryarray); + unset($parsed['query']); + + $parsed = array_merge($parsed, $queryarray); + } + + if (stripos($uri, 'redis') === 0) { + if (isset($parsed['pass'])) { + $parsed['password'] = $parsed['pass']; + unset($parsed['pass']); + } + + if (isset($parsed['path']) && preg_match('/^\/(\d+)(\/.*)?/', $parsed['path'], $path)) { + $parsed['database'] = $path[1]; + + if (isset($path[2])) { + $parsed['path'] = $path[2]; + } else { + unset($parsed['path']); + } + } + } + + 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'); + } +} diff --git a/predis/src/Connection/ParametersInterface.php b/predis/src/Connection/ParametersInterface.php new file mode 100644 index 0000000..fd8a908 --- /dev/null +++ b/predis/src/Connection/ParametersInterface.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +/** + * 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 + */ +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(); +} diff --git a/predis/src/Connection/PhpiredisSocketConnection.php b/predis/src/Connection/PhpiredisSocketConnection.php new file mode 100644 index 0000000..edded2d --- /dev/null +++ b/predis/src/Connection/PhpiredisSocketConnection.php @@ -0,0 +1,418 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\CommandInterface; +use Predis\NotSupportedException; +use Predis\Response\Error as ErrorResponse; +use Predis\Response\ErrorInterface as ErrorResponseInterface; +use Predis\Response\Status as StatusResponse; + +/** + * 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 'redis', '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 (default is 5 seconds). + * - read_write_timeout: timeout of read / write operations. + * + * @link http://github.com/nrk/phpiredis + * + * @author Daniele Alessandri + */ +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) + { + switch ($parameters->scheme) { + case 'tcp': + case 'redis': + case 'unix': + break; + + default: + throw new \InvalidArgumentException("Invalid scheme: '$parameters->scheme'."); + } + + if (isset($parameters->persistent)) { + throw new NotSupportedException( + 'Persistent connections are not supported by this connection backend.' + ); + } + + return $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 + */ + protected function getStatusHandler() + { + static $statusHandler; + + if (!$statusHandler) { + $statusHandler = function ($payload) { + return StatusResponse::get($payload); + }; + } + + return $statusHandler; + } + + /** + * Returns the handler used by the protocol reader for error responses. + * + * @return \Closure + */ + protected function getErrorHandler() + { + static $errorHandler; + + if (!$errorHandler) { + $errorHandler = function ($errorMessage) { + return new ErrorResponse($errorMessage); + }; + } + + return $errorHandler; + } + + /** + * 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); + } + + /** + * Gets the address of an host from connection parameters. + * + * @param ParametersInterface $parameters Parameters used to initialize the connection. + * + * @return string + */ + protected static function getAddress(ParametersInterface $parameters) + { + if (filter_var($host = $parameters->host, FILTER_VALIDATE_IP)) { + return $host; + } + + if ($host === $address = gethostbyname($host)) { + return false; + } + + return $address; + } + + /** + * {@inheritdoc} + */ + protected function createResource() + { + $parameters = $this->parameters; + + if ($parameters->scheme === 'unix') { + $address = $parameters->path; + $domain = AF_UNIX; + $protocol = 0; + } else { + if (false === $address = self::getAddress($parameters)) { + $this->onConnectionError("Cannot resolve the address of '$parameters->host'."); + } + + $domain = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET; + $protocol = SOL_TCP; + } + + $socket = @socket_create($domain, SOCK_STREAM, $protocol); + + if (!is_resource($socket)) { + $this->emitSocketError(); + } + + $this->setSocketOptions($socket, $parameters); + $this->connectWithTimeout($socket, $address, $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 !== 'unix') { + 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(); + } + } + } + + /** + * Opens the actual connection to the server with a timeout. + * + * @param resource $socket Socket resource. + * @param string $address IP address (DNS-resolved from hostname) + * @param ParametersInterface $parameters Parameters used to initialize the connection. + * + * @return string + */ + private function connectWithTimeout($socket, $address, ParametersInterface $parameters) + { + socket_set_nonblock($socket); + + if (@socket_connect($socket, $address, (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 = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); + $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->initCommands) { + foreach ($this->initCommands as $command) { + $response = $this->executeCommand($command); + + if ($response instanceof ErrorResponseInterface) { + $this->onConnectionError("`{$command->getId()}` failed: $response", 0); + } + } + } + } + + /** + * {@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 === '' || $buffer === null) { + $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(); + } +} diff --git a/predis/src/Connection/PhpiredisStreamConnection.php b/predis/src/Connection/PhpiredisStreamConnection.php new file mode 100644 index 0000000..f0b719b --- /dev/null +++ b/predis/src/Connection/PhpiredisStreamConnection.php @@ -0,0 +1,238 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\CommandInterface; +use Predis\NotSupportedException; +use Predis\Response\Error as ErrorResponse; +use Predis\Response\Status as StatusResponse; + +/** + * 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 'redis', '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 + */ +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 assertSslSupport(ParametersInterface $parameters) + { + throw new \InvalidArgumentException('SSL encryption is not supported by this connection backend.'); + } + + /** + * {@inheritdoc} + */ + protected function createStreamSocket(ParametersInterface $parameters, $address, $flags, $context = null) + { + $socket = null; + $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); + + $resource = @stream_socket_client($address, $errno, $errstr, $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() + { + static $statusHandler; + + if (!$statusHandler) { + $statusHandler = function ($payload) { + return StatusResponse::get($payload); + }; + } + + return $statusHandler; + } + + /** + * Returns the handler used by the protocol reader for error responses. + * + * @return \Closure + */ + protected function getErrorHandler() + { + static $errorHandler; + + if (!$errorHandler) { + $errorHandler = function ($errorMessage) { + return new ErrorResponse($errorMessage); + }; + } + + return $errorHandler; + } + + /** + * {@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(); + } +} diff --git a/predis/src/Connection/StreamConnection.php b/predis/src/Connection/StreamConnection.php new file mode 100644 index 0000000..9c26272 --- /dev/null +++ b/predis/src/Connection/StreamConnection.php @@ -0,0 +1,396 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\CommandInterface; +use Predis\Response\Error as ErrorResponse; +use Predis\Response\ErrorInterface as ErrorResponseInterface; +use Predis\Response\Status as StatusResponse; + +/** + * 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 'redis', 'tcp', 'rediss', 'tls' 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 (default is 5 seconds). + * - 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. + * - ssl: context options array (see http://php.net/manual/en/context.ssl.php) + * + * @author Daniele Alessandri + */ +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 assertParameters(ParametersInterface $parameters) + { + switch ($parameters->scheme) { + case 'tcp': + case 'redis': + case 'unix': + break; + + case 'tls': + case 'rediss': + $this->assertSslSupport($parameters); + break; + + default: + throw new \InvalidArgumentException("Invalid scheme: '$parameters->scheme'."); + } + + return $parameters; + } + + /** + * Checks needed conditions for SSL-encrypted connections. + * + * @param ParametersInterface $parameters Initialization parameters for the connection. + * + * @throws \InvalidArgumentException + */ + protected function assertSslSupport(ParametersInterface $parameters) + { + if ( + filter_var($parameters->persistent, FILTER_VALIDATE_BOOLEAN) && + version_compare(PHP_VERSION, '7.0.0beta') < 0 + ) { + throw new \InvalidArgumentException('Persistent SSL connections require PHP >= 7.0.0.'); + } + } + + /** + * {@inheritdoc} + */ + protected function createResource() + { + switch ($this->parameters->scheme) { + case 'tcp': + case 'redis': + return $this->tcpStreamInitializer($this->parameters); + + case 'unix': + return $this->unixStreamInitializer($this->parameters); + + case 'tls': + case 'rediss': + return $this->tlsStreamInitializer($this->parameters); + + default: + throw new \InvalidArgumentException("Invalid scheme: '{$this->parameters->scheme}'."); + } + } + + /** + * Creates a connected stream socket resource. + * + * @param ParametersInterface $parameters Connection parameters. + * @param string $address Address for stream_socket_client(). + * @param int $flags Flags for stream_socket_client(). + * + * @return resource + */ + protected function createStreamSocket(ParametersInterface $parameters, $address, $flags) + { + $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); + + if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags)) { + $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 TCP stream resource. + * + * @param ParametersInterface $parameters Initialization parameters for the connection. + * + * @return resource + */ + protected function tcpStreamInitializer(ParametersInterface $parameters) + { + if (!filter_var($parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $address = "tcp://$parameters->host:$parameters->port"; + } else { + $address = "tcp://[$parameters->host]:$parameters->port"; + } + + $flags = STREAM_CLIENT_CONNECT; + + if (isset($parameters->async_connect) && $parameters->async_connect) { + $flags |= STREAM_CLIENT_ASYNC_CONNECT; + } + + if (isset($parameters->persistent)) { + if (false !== $persistent = filter_var($parameters->persistent, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { + $flags |= STREAM_CLIENT_PERSISTENT; + + if ($persistent === null) { + $address = "{$address}/{$parameters->persistent}"; + } + } + } + + $resource = $this->createStreamSocket($parameters, $address, $flags); + + return $resource; + } + + /** + * Initializes a UNIX stream resource. + * + * @param ParametersInterface $parameters Initialization parameters for the connection. + * + * @return resource + */ + protected function unixStreamInitializer(ParametersInterface $parameters) + { + if (!isset($parameters->path)) { + throw new \InvalidArgumentException('Missing UNIX domain socket path.'); + } + + $flags = STREAM_CLIENT_CONNECT; + + if (isset($parameters->persistent)) { + if (false !== $persistent = filter_var($parameters->persistent, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { + $flags |= STREAM_CLIENT_PERSISTENT; + + if ($persistent === null) { + throw new \InvalidArgumentException( + 'Persistent connection IDs are not supported when using UNIX domain sockets.' + ); + } + } + } + + $resource = $this->createStreamSocket($parameters, "unix://{$parameters->path}", $flags); + + return $resource; + } + + /** + * Initializes a SSL-encrypted TCP stream resource. + * + * @param ParametersInterface $parameters Initialization parameters for the connection. + * + * @return resource + */ + protected function tlsStreamInitializer(ParametersInterface $parameters) + { + $resource = $this->tcpStreamInitializer($parameters); + $metadata = stream_get_meta_data($resource); + + // Detect if crypto mode is already enabled for this stream (PHP >= 7.0.0). + if (isset($metadata['crypto'])) { + return $resource; + } + + if (is_array($parameters->ssl)) { + $options = $parameters->ssl; + } else { + $options = array(); + } + + if (!isset($options['crypto_type'])) { + $options['crypto_type'] = STREAM_CRYPTO_METHOD_TLS_CLIENT; + } + + if (!stream_context_set_option($resource, array('ssl' => $options))) { + $this->onConnectionError('Error while setting SSL context options'); + } + + if (!stream_socket_enable_crypto($resource, true, $options['crypto_type'])) { + $this->onConnectionError('Error while switching to encrypted communication'); + } + + return $resource; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if (parent::connect() && $this->initCommands) { + foreach ($this->initCommands as $command) { + $response = $this->executeCommand($command); + + if ($response instanceof ErrorResponseInterface) { + $this->onConnectionError("`{$command->getId()}` failed: $response", 0); + } + } + } + } + + /** + * {@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; + } + + $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; + } + + $multibulk = array(); + + for ($i = 0; $i < $count; ++$i) { + $multibulk[$i] = $this->read(); + } + + return $multibulk; + + case ':': + $integer = (int) $payload; + return $integer == $payload ? $integer : $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"; + + foreach ($arguments as $argument) { + $arglen = strlen($argument); + $buffer .= "\${$arglen}\r\n{$argument}\r\n"; + } + + $this->write($buffer); + } +} diff --git a/predis/src/Connection/WebdisConnection.php b/predis/src/Connection/WebdisConnection.php new file mode 100644 index 0000000..c8d6e50 --- /dev/null +++ b/predis/src/Connection/WebdisConnection.php @@ -0,0 +1,366 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Connection; + +use Predis\Command\CommandInterface; +use Predis\NotSupportedException; +use Predis\Protocol\ProtocolException; +use Predis\Response\Error as ErrorResponse; +use Predis\Response\Status as StatusResponse; + +/** + * 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 (default is 5 seconds). + * - 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 + */ +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(); + $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0) * 1000; + + if (filter_var($host = $parameters->host, FILTER_VALIDATE_IP)) { + $host = "[$host]"; + } + + $options = array( + CURLOPT_FAILONERROR => true, + CURLOPT_CONNECTTIMEOUT_MS => $timeout, + CURLOPT_URL => "$parameters->scheme://$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() + { + static $statusHandler; + + if (!$statusHandler) { + $statusHandler = function ($payload) { + return StatusResponse::get($payload); + }; + } + + return $statusHandler; + } + + /** + * Returns the handler used by the protocol reader for error responses. + * + * @return \Closure + */ + protected function getErrorHandler() + { + static $errorHandler; + + if (!$errorHandler) { + $errorHandler = function ($errorMessage) { + return new ErrorResponse($errorMessage); + }; + } + + return $errorHandler; + } + + /** + * 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. + * + * @throws NotSupportedException + * + * @return string + */ + 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(); + } +} diff --git a/predis/src/Monitor/Consumer.php b/predis/src/Monitor/Consumer.php new file mode 100644 index 0000000..aaf645b --- /dev/null +++ b/predis/src/Monitor/Consumer.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Monitor; + +use Predis\ClientInterface; +use Predis\Connection\AggregateConnectionInterface; +use Predis\NotSupportedException; + +/** + * Redis MONITOR consumer. + * + * @author Daniele Alessandri + */ +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, + ); + } +} diff --git a/predis/src/NotSupportedException.php b/predis/src/NotSupportedException.php new file mode 100644 index 0000000..be82aba --- /dev/null +++ b/predis/src/NotSupportedException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis; + +/** + * Exception class thrown when trying to use features not supported by certain + * classes or abstractions of Predis. + * + * @author Daniele Alessandri + */ +class NotSupportedException extends PredisException +{ +} diff --git a/predis/src/Pipeline/Atomic.php b/predis/src/Pipeline/Atomic.php new file mode 100644 index 0000000..1c9c92a --- /dev/null +++ b/predis/src/Pipeline/Atomic.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Pipeline; + +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; + +/** + * Command pipeline wrapped into a MULTI / EXEC transaction. + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Pipeline/ConnectionErrorProof.php b/predis/src/Pipeline/ConnectionErrorProof.php new file mode 100644 index 0000000..d3bc732 --- /dev/null +++ b/predis/src/Pipeline/ConnectionErrorProof.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Pipeline; + +use Predis\CommunicationException; +use Predis\Connection\Aggregate\ClusterInterface; +use Predis\Connection\ConnectionInterface; +use Predis\Connection\NodeConnectionInterface; +use Predis\NotSupportedException; + +/** + * 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 + */ +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; + } +} diff --git a/predis/src/Pipeline/FireAndForget.php b/predis/src/Pipeline/FireAndForget.php new file mode 100644 index 0000000..95a062b --- /dev/null +++ b/predis/src/Pipeline/FireAndForget.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Pipeline; + +use Predis\Connection\ConnectionInterface; + +/** + * Command pipeline that writes commands to the servers but discards responses. + * + * @author Daniele Alessandri + */ +class FireAndForget extends Pipeline +{ + /** + * {@inheritdoc} + */ + protected function executePipeline(ConnectionInterface $connection, \SplQueue $commands) + { + while (!$commands->isEmpty()) { + $connection->writeRequest($commands->dequeue()); + } + + $connection->disconnect(); + + return array(); + } +} diff --git a/predis/src/Pipeline/Pipeline.php b/predis/src/Pipeline/Pipeline.php new file mode 100644 index 0000000..cf9c59e --- /dev/null +++ b/predis/src/Pipeline/Pipeline.php @@ -0,0 +1,247 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Pipeline; + +use Predis\ClientContextInterface; +use Predis\ClientException; +use Predis\ClientInterface; +use Predis\Command\CommandInterface; +use Predis\Connection\Aggregate\ReplicationInterface; +use Predis\Connection\ConnectionInterface; +use Predis\Response\ErrorInterface as ErrorResponseInterface; +use Predis\Response\ResponseInterface; +use Predis\Response\ServerException; + +/** + * 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 + */ +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. + * + * @throws \Exception + * @throws \InvalidArgumentException + * + * @return array + */ + 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; + } +} diff --git a/predis/src/PredisException.php b/predis/src/PredisException.php new file mode 100644 index 0000000..122bde1 --- /dev/null +++ b/predis/src/PredisException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis; + +/** + * Base exception class for Predis-related errors. + * + * @author Daniele Alessandri + */ +abstract class PredisException extends \Exception +{ +} diff --git a/predis/src/Profile/Factory.php b/predis/src/Profile/Factory.php new file mode 100644 index 0000000..d4907a2 --- /dev/null +++ b/predis/src/Profile/Factory.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +use Predis\ClientException; + +/** + * Factory class for creating profile instances from strings. + * + * @author Daniele Alessandri + */ +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', + '3.2' => 'Predis\Profile\RedisVersion320', + 'dev' => 'Predis\Profile\RedisUnstable', + 'default' => 'Predis\Profile\RedisVersion320', + ); + + /** + * + */ + 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. + * + * @throws ClientException + * + * @return ProfileInterface + */ + public static function get($version) + { + if (!isset(self::$profiles[$version])) { + throw new ClientException("Unknown server profile: '$version'."); + } + + $profile = self::$profiles[$version]; + + return new $profile(); + } +} diff --git a/predis/src/Profile/ProfileInterface.php b/predis/src/Profile/ProfileInterface.php new file mode 100644 index 0000000..abe71aa --- /dev/null +++ b/predis/src/Profile/ProfileInterface.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +use Predis\Command\CommandInterface; + +/** + * 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 + */ +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()); +} diff --git a/predis/src/Profile/RedisProfile.php b/predis/src/Profile/RedisProfile.php new file mode 100644 index 0000000..3ef3168 --- /dev/null +++ b/predis/src/Profile/RedisProfile.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +use Predis\ClientException; +use Predis\Command\Processor\ProcessorInterface; + +/** + * Base class implementing common functionalities for Redis server profiles. + * + * @author Daniele Alessandri + */ +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(); + } +} diff --git a/predis/src/Profile/RedisUnstable.php b/predis/src/Profile/RedisUnstable.php new file mode 100644 index 0000000..573cc9e --- /dev/null +++ b/predis/src/Profile/RedisUnstable.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +/** + * Server profile for the current unstable version of Redis. + * + * @author Daniele Alessandri + */ +class RedisUnstable extends RedisVersion320 +{ + /** + * {@inheritdoc} + */ + public function getVersion() + { + return '3.2'; + } + + /** + * {@inheritdoc} + */ + public function getSupportedCommands() + { + return array_merge(parent::getSupportedCommands(), array( + // EMPTY + )); + } +} diff --git a/predis/src/Profile/RedisVersion200.php b/predis/src/Profile/RedisVersion200.php new file mode 100644 index 0000000..234d53c --- /dev/null +++ b/predis/src/Profile/RedisVersion200.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +/** + * Server profile for Redis 2.0. + * + * @author Daniele Alessandri + */ +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', + ); + } +} diff --git a/predis/src/Profile/RedisVersion220.php b/predis/src/Profile/RedisVersion220.php new file mode 100644 index 0000000..899014e --- /dev/null +++ b/predis/src/Profile/RedisVersion220.php @@ -0,0 +1,202 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +/** + * Server profile for Redis 2.2. + * + * @author Daniele Alessandri + */ +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', + ); + } +} diff --git a/predis/src/Profile/RedisVersion240.php b/predis/src/Profile/RedisVersion240.php new file mode 100644 index 0000000..0856c37 --- /dev/null +++ b/predis/src/Profile/RedisVersion240.php @@ -0,0 +1,207 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +/** + * Server profile for Redis 2.4. + * + * @author Daniele Alessandri + */ +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', + ); + } +} diff --git a/predis/src/Profile/RedisVersion260.php b/predis/src/Profile/RedisVersion260.php new file mode 100644 index 0000000..ba5084a --- /dev/null +++ b/predis/src/Profile/RedisVersion260.php @@ -0,0 +1,235 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +/** + * Server profile for Redis 2.6. + * + * @author Daniele Alessandri + */ +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', + 'MIGRATE' => 'Predis\Command\KeyMigrate', + + /* 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', + ); + } +} diff --git a/predis/src/Profile/RedisVersion280.php b/predis/src/Profile/RedisVersion280.php new file mode 100644 index 0000000..ea17e68 --- /dev/null +++ b/predis/src/Profile/RedisVersion280.php @@ -0,0 +1,267 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +/** + * Server profile for Redis 2.8. + * + * @author Daniele Alessandri + */ +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', + 'MIGRATE' => 'Predis\Command\KeyMigrate', + + /* 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', + 'ZREVRANGEBYLEX' => 'Predis\Command\ZSetReverseRangeByLex', + + /* 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', + ); + } +} diff --git a/predis/src/Profile/RedisVersion300.php b/predis/src/Profile/RedisVersion300.php new file mode 100644 index 0000000..8a2fac8 --- /dev/null +++ b/predis/src/Profile/RedisVersion300.php @@ -0,0 +1,270 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +/** + * Server profile for Redis 3.0. + * + * @author Daniele Alessandri + */ +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', + 'MIGRATE' => 'Predis\Command\KeyMigrate', + + /* 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', + 'ZREVRANGEBYLEX' => 'Predis\Command\ZSetReverseRangeByLex', + + /* 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 ---------------- */ + + ); + } +} diff --git a/predis/src/Profile/RedisVersion320.php b/predis/src/Profile/RedisVersion320.php new file mode 100644 index 0000000..7de7957 --- /dev/null +++ b/predis/src/Profile/RedisVersion320.php @@ -0,0 +1,281 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Profile; + +/** + * Server profile for Redis 3.0. + * + * @author Daniele Alessandri + */ +class RedisVersion320 extends RedisProfile +{ + /** + * {@inheritdoc} + */ + public function getVersion() + { + return '3.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', + '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', + 'MIGRATE' => 'Predis\Command\KeyMigrate', + + /* 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', + 'ZREVRANGEBYLEX' => 'Predis\Command\ZSetReverseRangeByLex', + + /* 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.2 ---------------- */ + + /* commands operating on hashes */ + 'HSTRLEN' => 'Predis\Command\HashStringLength', + 'BITFIELD' => 'Predis\Command\StringBitField', + + /* commands performing geospatial operations */ + 'GEOADD' => 'Predis\Command\GeospatialGeoAdd', + 'GEOHASH' => 'Predis\Command\GeospatialGeoHash', + 'GEOPOS' => 'Predis\Command\GeospatialGeoPos', + 'GEODIST' => 'Predis\Command\GeospatialGeoDist', + 'GEORADIUS' => 'Predis\Command\GeospatialGeoRadius', + 'GEORADIUSBYMEMBER' => 'Predis\Command\GeospatialGeoRadiusByMember', + ); + } +} diff --git a/predis/src/Protocol/ProtocolException.php b/predis/src/Protocol/ProtocolException.php new file mode 100644 index 0000000..6fe5d6d --- /dev/null +++ b/predis/src/Protocol/ProtocolException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol; + +use Predis\CommunicationException; + +/** + * Exception used to indentify errors encountered while parsing the Redis wire + * protocol. + * + * @author Daniele Alessandri + */ +class ProtocolException extends CommunicationException +{ +} diff --git a/predis/src/Protocol/ProtocolProcessorInterface.php b/predis/src/Protocol/ProtocolProcessorInterface.php new file mode 100644 index 0000000..b34ea18 --- /dev/null +++ b/predis/src/Protocol/ProtocolProcessorInterface.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol; + +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 + */ +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); +} diff --git a/predis/src/Protocol/RequestSerializerInterface.php b/predis/src/Protocol/RequestSerializerInterface.php new file mode 100644 index 0000000..eef72a6 --- /dev/null +++ b/predis/src/Protocol/RequestSerializerInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol; + +use Predis\Command\CommandInterface; + +/** + * Defines a pluggable serializer for Redis commands. + * + * @author Daniele Alessandri + */ +interface RequestSerializerInterface +{ + /** + * Serializes a Redis command. + * + * @param CommandInterface $command Redis command. + * + * @return string + */ + public function serialize(CommandInterface $command); +} diff --git a/predis/src/Protocol/ResponseReaderInterface.php b/predis/src/Protocol/ResponseReaderInterface.php new file mode 100644 index 0000000..86a7bdc --- /dev/null +++ b/predis/src/Protocol/ResponseReaderInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol; + +use Predis\Connection\CompositeConnectionInterface; + +/** + * Defines a pluggable reader capable of parsing responses returned by Redis and + * deserializing them to PHP objects. + * + * @author Daniele Alessandri + */ +interface ResponseReaderInterface +{ + /** + * Reads a response from a connection to Redis. + * + * @param CompositeConnectionInterface $connection Redis connection. + * + * @return mixed + */ + public function read(CompositeConnectionInterface $connection); +} diff --git a/predis/src/Protocol/Text/CompositeProtocolProcessor.php b/predis/src/Protocol/Text/CompositeProtocolProcessor.php new file mode 100644 index 0000000..ea85ed3 --- /dev/null +++ b/predis/src/Protocol/Text/CompositeProtocolProcessor.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text; + +use Predis\Command\CommandInterface; +use Predis\Connection\CompositeConnectionInterface; +use Predis\Protocol\ProtocolProcessorInterface; +use Predis\Protocol\RequestSerializerInterface; +use Predis\Protocol\ResponseReaderInterface; + +/** + * 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 + */ +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; + } +} diff --git a/predis/src/Protocol/Text/Handler/BulkResponse.php b/predis/src/Protocol/Text/Handler/BulkResponse.php new file mode 100644 index 0000000..5b0bf3c --- /dev/null +++ b/predis/src/Protocol/Text/Handler/BulkResponse.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text\Handler; + +use Predis\CommunicationException; +use Predis\Connection\CompositeConnectionInterface; +use Predis\Protocol\ProtocolException; + +/** + * 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 + */ +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; + } + + CommunicationException::handle(new ProtocolException( + $connection, "Value '$payload' is not a valid length for a bulk response." + )); + + return; + } +} diff --git a/predis/src/Protocol/Text/Handler/ErrorResponse.php b/predis/src/Protocol/Text/Handler/ErrorResponse.php new file mode 100644 index 0000000..3e18b7b --- /dev/null +++ b/predis/src/Protocol/Text/Handler/ErrorResponse.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text\Handler; + +use Predis\Connection\CompositeConnectionInterface; +use Predis\Response\Error; + +/** + * 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 + */ +class ErrorResponse implements ResponseHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handle(CompositeConnectionInterface $connection, $payload) + { + return new Error($payload); + } +} diff --git a/predis/src/Protocol/Text/Handler/IntegerResponse.php b/predis/src/Protocol/Text/Handler/IntegerResponse.php new file mode 100644 index 0000000..f965601 --- /dev/null +++ b/predis/src/Protocol/Text/Handler/IntegerResponse.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text\Handler; + +use Predis\CommunicationException; +use Predis\Connection\CompositeConnectionInterface; +use Predis\Protocol\ProtocolException; + +/** + * 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 + */ +class IntegerResponse implements ResponseHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handle(CompositeConnectionInterface $connection, $payload) + { + if (is_numeric($payload)) { + $integer = (int) $payload; + return $integer == $payload ? $integer : $payload; + } + + if ($payload !== 'nil') { + CommunicationException::handle(new ProtocolException( + $connection, "Cannot parse '$payload' as a valid numeric response." + )); + } + + return; + } +} diff --git a/predis/src/Protocol/Text/Handler/MultiBulkResponse.php b/predis/src/Protocol/Text/Handler/MultiBulkResponse.php new file mode 100644 index 0000000..820b9b4 --- /dev/null +++ b/predis/src/Protocol/Text/Handler/MultiBulkResponse.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text\Handler; + +use Predis\CommunicationException; +use Predis\Connection\CompositeConnectionInterface; +use Predis\Protocol\ProtocolException; + +/** + * 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 + */ +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; + } + + $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; + } +} diff --git a/predis/src/Protocol/Text/Handler/ResponseHandlerInterface.php b/predis/src/Protocol/Text/Handler/ResponseHandlerInterface.php new file mode 100644 index 0000000..ca08a9c --- /dev/null +++ b/predis/src/Protocol/Text/Handler/ResponseHandlerInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text\Handler; + +use Predis\Connection\CompositeConnectionInterface; + +/** + * Defines a pluggable handler used to parse a particular type of response. + * + * @author Daniele Alessandri + */ +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); +} diff --git a/predis/src/Protocol/Text/Handler/StatusResponse.php b/predis/src/Protocol/Text/Handler/StatusResponse.php new file mode 100644 index 0000000..7bde555 --- /dev/null +++ b/predis/src/Protocol/Text/Handler/StatusResponse.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text\Handler; + +use Predis\Connection\CompositeConnectionInterface; +use Predis\Response\Status; + +/** + * 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 + */ +class StatusResponse implements ResponseHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handle(CompositeConnectionInterface $connection, $payload) + { + return Status::get($payload); + } +} diff --git a/predis/src/Protocol/Text/Handler/StreamableMultiBulkResponse.php b/predis/src/Protocol/Text/Handler/StreamableMultiBulkResponse.php new file mode 100644 index 0000000..7cdb736 --- /dev/null +++ b/predis/src/Protocol/Text/Handler/StreamableMultiBulkResponse.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text\Handler; + +use Predis\CommunicationException; +use Predis\Connection\CompositeConnectionInterface; +use Predis\Protocol\ProtocolException; +use Predis\Response\Iterator\MultiBulk as MultiBulkIterator; + +/** + * 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 + */ +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); + } +} diff --git a/predis/src/Protocol/Text/ProtocolProcessor.php b/predis/src/Protocol/Text/ProtocolProcessor.php new file mode 100644 index 0000000..99acdf8 --- /dev/null +++ b/predis/src/Protocol/Text/ProtocolProcessor.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text; + +use Predis\Command\CommandInterface; +use Predis\CommunicationException; +use Predis\Connection\CompositeConnectionInterface; +use Predis\Protocol\ProtocolException; +use Predis\Protocol\ProtocolProcessorInterface; +use Predis\Response\Error as ErrorResponse; +use Predis\Response\Iterator\MultiBulk as MultiBulkIterator; +use Predis\Response\Status as StatusResponse; + +/** + * Protocol processor for the standard Redis wire protocol. + * + * @link http://redis.io/topics/protocol + * + * @author Daniele Alessandri + */ +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; + } + + return substr($connection->readBuffer($size + 2), 0, -2); + + case '*': + $count = (int) $payload; + + if ($count === -1) { + return; + } + if ($this->mbiterable) { + return new MultiBulkIterator($connection, $count); + } + + $multibulk = array(); + + for ($i = 0; $i < $count; ++$i) { + $multibulk[$i] = $this->read($connection); + } + + return $multibulk; + + case ':': + $integer = (int) $payload; + return $integer == $payload ? $integer : $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; + } +} diff --git a/predis/src/Protocol/Text/RequestSerializer.php b/predis/src/Protocol/Text/RequestSerializer.php new file mode 100644 index 0000000..859595b --- /dev/null +++ b/predis/src/Protocol/Text/RequestSerializer.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text; + +use Predis\Command\CommandInterface; +use Predis\Protocol\RequestSerializerInterface; + +/** + * Request serializer for the standard Redis wire protocol. + * + * @link http://redis.io/topics/protocol + * + * @author Daniele Alessandri + */ +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"; + + foreach ($arguments as $argument) { + $arglen = strlen($argument); + $buffer .= "\${$arglen}\r\n{$argument}\r\n"; + } + + return $buffer; + } +} diff --git a/predis/src/Protocol/Text/ResponseReader.php b/predis/src/Protocol/Text/ResponseReader.php new file mode 100644 index 0000000..d96218d --- /dev/null +++ b/predis/src/Protocol/Text/ResponseReader.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Protocol\Text; + +use Predis\CommunicationException; +use Predis\Connection\CompositeConnectionInterface; +use Predis\Protocol\ProtocolException; +use Predis\Protocol\ResponseReaderInterface; + +/** + * Response reader for the standard Redis wire protocol. + * + * @link http://redis.io/topics/protocol + * + * @author Daniele Alessandri + */ +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; + } + + /** + * {@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) + ); + } +} diff --git a/predis/src/PubSub/AbstractConsumer.php b/predis/src/PubSub/AbstractConsumer.php new file mode 100644 index 0000000..8c6a71d --- /dev/null +++ b/predis/src/PubSub/AbstractConsumer.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\PubSub; + +/** + * Base implementation of a PUB/SUB consumer abstraction based on PHP iterators. + * + * @author Daniele Alessandri + */ +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(); +} diff --git a/predis/src/PubSub/Consumer.php b/predis/src/PubSub/Consumer.php new file mode 100644 index 0000000..5f2d8a8 --- /dev/null +++ b/predis/src/PubSub/Consumer.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\PubSub; + +use Predis\ClientException; +use Predis\ClientInterface; +use Predis\Command\Command; +use Predis\Connection\AggregateConnectionInterface; +use Predis\NotSupportedException; + +/** + * PUB/SUB consumer abstraction. + * + * @author Daniele Alessandri + */ +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." + ); + } + } +} diff --git a/predis/src/PubSub/DispatcherLoop.php b/predis/src/PubSub/DispatcherLoop.php new file mode 100644 index 0000000..d0369e7 --- /dev/null +++ b/predis/src/PubSub/DispatcherLoop.php @@ -0,0 +1,170 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\PubSub; + +/** + * Method-dispatcher loop built around the client-side abstraction of a Redis + * PUB / SUB context. + * + * @author Daniele Alessandri + */ +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 ''; + } +} diff --git a/predis/src/Replication/MissingMasterException.php b/predis/src/Replication/MissingMasterException.php new file mode 100644 index 0000000..223bd2d --- /dev/null +++ b/predis/src/Replication/MissingMasterException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Replication; + +use Predis\ClientException; + +/** + * Exception class that identifies when master is missing in a replication setup. + * + * @author Daniele Alessandri + */ +class MissingMasterException extends ClientException +{ +} diff --git a/predis/src/Replication/ReplicationStrategy.php b/predis/src/Replication/ReplicationStrategy.php new file mode 100644 index 0000000..cd2d0ed --- /dev/null +++ b/predis/src/Replication/ReplicationStrategy.php @@ -0,0 +1,304 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Replication; + +use Predis\Command\CommandInterface; +use Predis\NotSupportedException; + +/** + * Defines a strategy for master/slave replication. + * + * @author Daniele Alessandri + */ +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. + * + * @throws NotSupportedException + * + * @return bool + */ + 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(); + $argc = count($arguments); + + if ($argc > 1) { + for ($i = 1; $i < $argc; ++$i) { + $argument = strtoupper($arguments[$i]); + if ($argument === 'STORE') { + return false; + } + } + } + + return true; + } + + /** + * Checks if BITFIELD performs a read-only operation by looking for certain + * SET and INCRYBY modifiers in the arguments array of the command. + * + * @param CommandInterface $command Command instance. + * + * @return bool + */ + protected function isBitfieldReadOnly(CommandInterface $command) + { + $arguments = $command->getArguments(); + $argc = count($arguments); + + if ($argc >= 2) { + for ($i = 1; $i < $argc; ++$i) { + $argument = strtoupper($arguments[$i]); + if ($argument === 'SET' || $argument === 'INCRBY') { + return false; + } + } + } + + return true; + } + + /** + * Checks if a GEORADIUS command is a readable operation by parsing the + * arguments array of the specified commad instance. + * + * @param CommandInterface $command Command instance. + * + * @return bool + */ + protected function isGeoradiusReadOnly(CommandInterface $command) + { + $arguments = $command->getArguments(); + $argc = count($arguments); + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; + + if ($argc > $startIndex) { + for ($i = $startIndex; $i < $argc; ++$i) { + $argument = strtoupper($arguments[$i]); + if ($argument === 'STORE' || $argument === 'STOREDIST') { + return false; + } + } + } + + return true; + } + + /** + * 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, + 'ZREVRANGEBYLEX' => true, + 'HGET' => true, + 'HMGET' => true, + 'HEXISTS' => true, + 'HLEN' => true, + 'HKEYS' => true, + 'HVALS' => true, + 'HGETALL' => true, + 'HSCAN' => true, + 'HSTRLEN' => true, + 'PING' => true, + 'AUTH' => true, + 'SELECT' => true, + 'ECHO' => true, + 'QUIT' => true, + 'OBJECT' => true, + 'BITCOUNT' => true, + 'BITPOS' => true, + 'TIME' => true, + 'PFCOUNT' => true, + 'SORT' => array($this, 'isSortReadOnly'), + 'BITFIELD' => array($this, 'isBitfieldReadOnly'), + 'GEOHASH' => true, + 'GEOPOS' => true, + 'GEODIST' => true, + 'GEORADIUS' => array($this, 'isGeoradiusReadOnly'), + 'GEORADIUSBYMEMBER' => array($this, 'isGeoradiusReadOnly'), + ); + } +} diff --git a/predis/src/Replication/RoleException.php b/predis/src/Replication/RoleException.php new file mode 100644 index 0000000..0d9954b --- /dev/null +++ b/predis/src/Replication/RoleException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Replication; + +use Predis\CommunicationException; + +/** + * Exception class that identifies a role mismatch when connecting to node + * managed by redis-sentinel. + * + * @author Daniele Alessandri + */ +class RoleException extends CommunicationException +{ +} diff --git a/predis/src/Response/Error.php b/predis/src/Response/Error.php new file mode 100644 index 0000000..3933857 --- /dev/null +++ b/predis/src/Response/Error.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Response; + +/** + * Represents an error returned by Redis (-ERR responses) during the execution + * of a command on the server. + * + * @author Daniele Alessandri + */ +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(); + } +} diff --git a/predis/src/Response/ErrorInterface.php b/predis/src/Response/ErrorInterface.php new file mode 100644 index 0000000..a4a4a02 --- /dev/null +++ b/predis/src/Response/ErrorInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Response; + +/** + * 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 + */ +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(); +} diff --git a/predis/src/Response/Iterator/MultiBulk.php b/predis/src/Response/Iterator/MultiBulk.php new file mode 100644 index 0000000..b1d2924 --- /dev/null +++ b/predis/src/Response/Iterator/MultiBulk.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Response\Iterator; + +use Predis\Connection\NodeConnectionInterface; + +/** + * Streamable multibulk response. + * + * @author Daniele Alessandri + */ +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(); + } +} diff --git a/predis/src/Response/Iterator/MultiBulkIterator.php b/predis/src/Response/Iterator/MultiBulkIterator.php new file mode 100644 index 0000000..5d32886 --- /dev/null +++ b/predis/src/Response/Iterator/MultiBulkIterator.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Response\Iterator; + +use Predis\Response\ResponseInterface; + +/** + * 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 + */ +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(); +} diff --git a/predis/src/Response/Iterator/MultiBulkTuple.php b/predis/src/Response/Iterator/MultiBulkTuple.php new file mode 100644 index 0000000..2b6f593 --- /dev/null +++ b/predis/src/Response/Iterator/MultiBulkTuple.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Response\Iterator; + +/** + * 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 + */ +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); + } +} diff --git a/predis/src/Response/ResponseInterface.php b/predis/src/Response/ResponseInterface.php new file mode 100644 index 0000000..0af1357 --- /dev/null +++ b/predis/src/Response/ResponseInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Response; + +/** + * Represents a complex response object from Redis. + * + * @author Daniele Alessandri + */ +interface ResponseInterface +{ +} diff --git a/predis/src/Response/ServerException.php b/predis/src/Response/ServerException.php new file mode 100644 index 0000000..407dc5b --- /dev/null +++ b/predis/src/Response/ServerException.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Response; + +use Predis\PredisException; + +/** + * Exception class that identifies server-side Redis errors. + * + * @author Daniele Alessandri + */ +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()); + } +} diff --git a/predis/src/Response/Status.php b/predis/src/Response/Status.php new file mode 100644 index 0000000..729bb66 --- /dev/null +++ b/predis/src/Response/Status.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Response; + +/** + * Represents a status response returned by Redis. + * + * @author Daniele Alessandri + */ +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); + } + } +} diff --git a/predis/src/Session/Handler.php b/predis/src/Session/Handler.php new file mode 100644 index 0000000..cecb9d5 --- /dev/null +++ b/predis/src/Session/Handler.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Session; + +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 + */ +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 (PHP_VERSION_ID >= 50400) { + 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; + } +} diff --git a/predis/src/Transaction/AbortedMultiExecException.php b/predis/src/Transaction/AbortedMultiExecException.php new file mode 100644 index 0000000..b36f38a --- /dev/null +++ b/predis/src/Transaction/AbortedMultiExecException.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Transaction; + +use Predis\PredisException; + +/** + * Exception class that identifies a MULTI / EXEC transaction aborted by Redis. + * + * @author Daniele Alessandri + */ +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; + } +} diff --git a/predis/src/Transaction/MultiExec.php b/predis/src/Transaction/MultiExec.php new file mode 100644 index 0000000..0cf1962 --- /dev/null +++ b/predis/src/Transaction/MultiExec.php @@ -0,0 +1,461 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Transaction; + +use Predis\ClientContextInterface; +use Predis\ClientException; +use Predis\ClientInterface; +use Predis\Command\CommandInterface; +use Predis\CommunicationException; +use Predis\Connection\AggregateConnectionInterface; +use Predis\NotSupportedException; +use Predis\Protocol\ProtocolException; +use Predis\Response\ErrorInterface as ErrorResponseInterface; +use Predis\Response\ServerException; +use Predis\Response\Status as StatusResponse; + +/** + * Client-side abstraction of a Redis transaction based on MULTI / EXEC. + * + * {@inheritdoc} + * + * @author Daniele Alessandri + */ +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. + * + * @throws ServerException + * + * @return mixed + */ + 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. + * + * @throws AbortedMultiExecException + * @throws CommunicationException + * + * @return $this|mixed + */ + 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. + * + * @throws NotSupportedException + * @throws ClientException + * + * @return mixed + */ + 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. + * + * @throws NotSupportedException + * + * @return MultiExec + */ + 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. + * + * @throws CommunicationException + * @throws AbortedMultiExecException + * @throws ServerException + * + * @return array + */ + 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; + } + + $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 + )); + } +} diff --git a/predis/src/Transaction/MultiExecState.php b/predis/src/Transaction/MultiExecState.php new file mode 100644 index 0000000..4bed42a --- /dev/null +++ b/predis/src/Transaction/MultiExecState.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Transaction; + +/** + * Utility class used to track the state of a MULTI / EXEC transaction. + * + * @author Daniele Alessandri + */ +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); + } +} diff --git a/secure.inc.php b/secure.inc.php new file mode 100644 index 0000000..c491eab --- /dev/null +++ b/secure.inc.php @@ -0,0 +1,11 @@ +'; + div += ''; + div += ''; + div += ''; + return div; +} + +window.agentOperateEvents = { + 'click .view': function (e, value, row, index) { + $('#form-agent-id').val(row.id); + $('#form-agent-ip').val(long2ip(row.ip)); + $('#form-agent-alias').val(row.alias); + $('#form-agent-token').val(row.token); + $('#form-agent-submit-type').val('view'); + $('#modal-agent').modal('show'); + }, + 'click .remove': function (e, value, row, index) { + if (!confirm('Are you sure to remove this agent?')) { + return; + } + var ajax = $.ajax({ + url: window.config.BASE_URL + "/service?action=agent_remove", + type: 'POST', + data: {id: row.id} + }); + ajax.done(function (res) { + if (res["errno"] !== 0) { + $("#modal-msg-content").html(res["msg"]); + $("#modal-msg").modal('show'); + } + $('#table-agent').bootstrapTable("refresh"); + }); + ajax.fail(function (jqXHR, textStatus) { + $("#modal-msg-content").html("Request failed : " + jqXHR.statusText); + $("#modal-msg").modal('show'); + $('#table-agent').bootstrapTable("refresh"); + }); + } +}; \ No newline at end of file diff --git a/static/config.js b/static/config.js new file mode 100644 index 0000000..ce092d3 --- /dev/null +++ b/static/config.js @@ -0,0 +1,5 @@ +window.config = { + 'BASE_URL': 'http://127.0.0.1' /* No '/' at the end */ + + +}; \ No newline at end of file diff --git a/static/job.js b/static/job.js new file mode 100755 index 0000000..ba218d9 --- /dev/null +++ b/static/job.js @@ -0,0 +1,233 @@ +function register_events_job() { + $('#btn-job-add').click(function (e) { + $('#modal-job').modal('show'); + }); + + $("#form-job-submit").click(function (e) { + var name = $('#form-job-name').val(); + var image = $('#form-job-image').val(); + var workspace = $('#form-job-workspace').val(); + var cluster = $('#form-job-cluster').val(); + var priority = $('#form-job-priority').val(); + var run_before = $('#form-job-run-before').val(); + if (run_before.length !== 0) { + run_before = moment(run_before).unix(); + } + var tasks = []; + + /* TODO validate form */ + + $('#modal-job').modal('hide'); + + var ajax = $.ajax({ + url: window.config.BASE_URL + "/service?action=job_submit", + type: 'POST', + data: { + name: name, + image: image, + workspace: workspace, + cluster: cluster, + priority: priority, + run_before: run_before, + tasks: JSON.stringify(tasks) + } + }); + ajax.done(function (res) { + if (res["errno"] !== 0) { + $("#modal-msg-content").html(res["msg"]); + $("#modal-msg").modal('show'); + } + $('#table-job').bootstrapTable("refresh"); + + }); + ajax.fail(function (jqXHR, textStatus) { + $("#modal-msg-content").html("Request failed : " + jqXHR.statusText); + $("#modal-msg").modal('show'); + $('#table-job').bootstrapTable("refresh"); + }); + }); + +} + +function load_jobs(scope) { + $("#table-job").bootstrapTable({ + url: window.config.BASE_URL + '/service?action=job_list&who=' + scope, + responseHandler: jobResponseHandler, + sidePagination: 'server', + cache: true, + striped: true, + pagination: true, + pageSize: 10, + pageList: [10, 25, 50, 100, 200], + search: false, + showColumns: true, + showRefresh: true, + showToggle: false, + showPaginationSwitch: true, + minimumCountColumns: 2, + clickToSelect: false, + sortName: 'nobody', + sortOrder: 'desc', + smartDisplay: true, + mobileResponsive: true, + showExport: true, + columns: [{ + field: 'created_by', + title: 'Created By', + align: 'center', + valign: 'middle', + formatter: UIDFormatter, + visible: scope === 'all' + }, { + field: 'name', + title: 'Name', + align: 'center', + valign: 'middle', + escape: true + }, { + field: 'image', + title: 'Docker Image', + align: 'center', + valign: 'middle', + visible: false, + escape: true + }, { + field: 'workspace', + title: 'Workspace', + align: 'center', + valign: 'middle', + visible: false, + formatter: workspaceFormatter + }, { + field: 'virtual_cluster', + title: 'Virtual Cluster', + align: 'center', + valign: 'middle', + formatter: clusterFormatter + }, { + field: 'priority', + title: 'Priority', + align: 'center', + valign: 'middle', + formatter: priorityFormatter + }, { + field: 'run_before', + title: 'Run Before', + align: 'center', + valign: 'middle', + visible: false, + formatter: timeFormatter + }, { + field: 'created_at', + title: 'Created At', + align: 'center', + valign: 'middle', + formatter: timeFormatter + }, { + field: 'status', + title: 'Status', + align: 'center', + valign: 'middle', + formatter: statusFormatter, + visible: true + }, { + field: 'operate', + title: 'Operate', + align: 'center', + events: jobOperateEvents, + formatter: jobOperateFormatter + }] + }); +} + +var UIDFormatter = function (UID) { + return UID; +}; + +var workspaceFormatter = function (workspace) { + return workspace; +}; + +var clusterFormatter = function (cluster) { + return cluster; +}; + +var priorityFormatter = function (status) { + switch (status) { + case '1': + return 'Low'; + case '25': + return 'Medium'; + case '50': + return 'High'; + case '99': + return 'Urgent'; + } + return 'Unknown (' + status + ')'; +}; + +var statusFormatter = function (status) { + switch (status) { + case '0': + return 'Pending'; + case '1': + return 'Running'; + case '2': + return 'Finished'; + case '3': + return 'Stopped'; + } + return 'Unknown(' + status + ')'; +}; + +function jobResponseHandler(res) { + if (res['errno'] === 0) { + var tmp = {}; + tmp["total"] = res["count"]; + tmp["rows"] = res["jobs"]; + return tmp; + } + $("#modal-msg-content").html(res["msg"]); + $("#modal-msg").modal('show'); + return []; +} + +function jobOperateFormatter(value, row, index) { + var div = '
'; + if (page_type === 'jobs') + div += ''; + if (page_type === 'jobs' && (row.status === '0' || row.status === '1')) + div += ''; + div += '
'; + return div; +} + +window.jobOperateEvents = { + 'click .view': function (e, value, row, index) { + var formattedData = JSON.stringify(row, null, '\t'); + $('#modal-job-description-content').text(formattedData); + $('#modal-job-description').modal('show'); + }, + 'click .stop': function (e, value, row, index) { + if (!confirm('Are you sure to stop this job?')) { + return; + } + var ajax = $.ajax({ + url: window.config.BASE_URL + "/service?action=job_stop", + type: 'POST', + data: {id: row.id} + }); + ajax.done(function (res) { + if (res["errno"] !== 0) { + $("#modal-msg-content").html(res["msg"]); + $("#modal-msg").modal('show'); + } + $('#table-link').bootstrapTable("refresh"); + }); + ajax.fail(function (jqXHR, textStatus) { + $("#modal-msg-content").html("Request failed : " + jqXHR.statusText); + $("#modal-msg").modal('show'); + $('#table-job').bootstrapTable("refresh"); + }); + } +}; \ No newline at end of file diff --git a/static/main.js b/static/main.js new file mode 100755 index 0000000..1b465a5 --- /dev/null +++ b/static/main.js @@ -0,0 +1,7 @@ +$(function () { + $('#main-tabs a').click(function (e) { + e.preventDefault(); + $(this).tab('show'); + }); + +}); \ No newline at end of file diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..7d93a75 --- /dev/null +++ b/static/script.js @@ -0,0 +1,27 @@ +$(function () { + $("#btn-signout").click(function (e) { + e.preventDefault(); + var ajax = $.ajax({ + url: window.config.BASE_URL + "/service?action=user_signout", + type: 'POST', + data: {} + }); + ajax.done(function (res) { + window.location.pathname = "/"; + }); + }); + + $("#btn-oauth-login").click(function (e) { + e.preventDefault(); + var ajax = $.ajax({ + url: window.config.BASE_URL + "/service?action=oauth_get_url", + type: 'POST', + data: {} + }); + ajax.done(function (res) { + window.location.href = res['url']; + }); + }); + + $('.date-picker').datetimepicker(); +}); \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100755 index 0000000..03c555e --- /dev/null +++ b/static/style.css @@ -0,0 +1,32 @@ + +/* make sticky footer */ +body, html { + height: 100%; +} + +footer, .push { + height: 36px; +} + +footer, footer .ul, footer .breadcrumb { + margin-bottom: 0; + background-color: transparent; + text-align: center; +} + +footer a:link, footer a:visited { + text-decoration: none; + color: #777; +} + +footer a:hover { + text-decoration: underline; + color: #000000; +} + +.wrapper { + min-height: 100%; + margin: 0 auto -36px; /* the bottom margin is the negative value of the footer's height */ +} + +/* end */ \ No newline at end of file diff --git a/static/ucenter.js b/static/ucenter.js new file mode 100755 index 0000000..7d7c0a7 --- /dev/null +++ b/static/ucenter.js @@ -0,0 +1,110 @@ +$(function () { + console.log(page_type); + switch (page_type) { + case "logs": + load_logs('self'); + break; + case "logs_all": + load_logs('all'); + break; + case "jobs": + register_events_job(); + load_jobs('self'); + break; + case "agents": + register_events_agent(); + load_agents(''); + break; + default: + break; + } +}); + +function load_logs(scope) { + $("#table-log").bootstrapTable({ + url: window.config.BASE_URL + '/service?action=log_gets&who=' + scope, + responseHandler: logResponseHandler, + sidePagination: 'server', + cache: true, + striped: true, + pagination: true, + pageSize: 10, + pageList: [10, 25, 50, 100, 200], + search: false, + showColumns: false, + showRefresh: false, + showToggle: false, + showPaginationSwitch: false, + minimumCountColumns: 2, + clickToSelect: false, + sortName: 'default', + sortOrder: 'desc', + smartDisplay: true, + mobileResponsive: true, + showExport: false, + columns: [{ + field: 'scope', + title: 'UID', + align: 'center', + valign: 'middle', + sortable: false, + visible: scope === 'all' + }, { + field: 'tag', + title: 'Tag', + align: 'center', + valign: 'middle', + sortable: false, + visible: scope === 'all' + }, { + field: 'time', + title: 'Time', + align: 'center', + valign: 'middle', + sortable: false, + formatter: timeFormatter + }, { + field: 'ip', + title: 'IP', + align: 'center', + valign: 'middle', + sortable: false, + formatter: long2ip + }, { + field: 'content', + title: 'Result', + align: 'center', + valign: 'middle', + sortable: false, + formatter: resultFormatter + }, { + field: 'content', + title: 'Content', + align: 'center', + valign: 'middle', + sortable: false, + visible: scope === 'all', + escape: true + }] + }); +} + +var logResponseHandler = function (res) { + if (res['errno'] === 0) { + var tmp = {}; + tmp["total"] = res["count"]; + tmp["rows"] = res["logs"]; + return tmp; + } + $("#modal-msg-content").html(res["msg"]); + $("#modal-msg").modal('show'); + return []; +}; + +var resultFormatter = function (json) { + var res = JSON.parse(json); + if (res['response'] === 0) { + return 'Success'; + } + return 'Fail'; +}; \ No newline at end of file diff --git a/static/util.js b/static/util.js new file mode 100644 index 0000000..9735e44 --- /dev/null +++ b/static/util.js @@ -0,0 +1,100 @@ +function timeFormatter(unixTimestamp) { + if (unixTimestamp === null) { + return null; + } + var d = new Date(unixTimestamp * 1000); + d.setTime(d.getTime() - d.getTimezoneOffset() * 60 * 1000); + return formatDate(d, '%Y-%M-%d %H:%m'); +} + +function formatDate(date, fmt) { + function pad(value) { + return (value.toString().length < 2) ? '0' + value : value; + } + + return fmt.replace(/%([a-zA-Z])/g, function (_, fmtCode) { + switch (fmtCode) { + case 'Y': + return date.getUTCFullYear(); + case 'M': + return pad(date.getUTCMonth() + 1); + case 'n': + return date.getUTCMonth() + 1; + case 'd': + return pad(date.getUTCDate()); + case 'H': + return pad(date.getUTCHours()); + case 'm': + return pad(date.getUTCMinutes()); + case 's': + return pad(date.getUTCSeconds()); + default: + throw new Error('Unsupported format code: ' + fmtCode); + } + }); +} + +function long2ip(ip) { + // discuss at: http://locutus.io/php/long2ip/ + // original by: Waldo Malqui Silva (http://waldo.malqui.info) + if (!isFinite(ip)) { + return false; + } + return [ip >>> 24, ip >>> 16 & 0xFF, ip >>> 8 & 0xFF, ip & 0xFF].join('.') +} + +//http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript +function getParameterByName(name, url) { + if (!url) { + url = window.location.href; + } + name = name.replace(/[\[\]]/g, "\\$&"); + var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, " ")); +} + +// https://stackoverflow.com/questions/736513/how-do-i-parse-a-url-into-hostname-and-path-in-javascript +function isURL(url) { + var parser = document.createElement('a'); + parser.href = url; + + var protocal = parser.protocol; // => "http:" + var host = parser.host; // => "example.com:3000" + var hostname = parser.hostname; // => "example.com" + var port = parser.port; // => "3000" + var pathname = parser.pathname; // => "/pathname/" + var hash = parser.hash; // => "#hash" + var search = parser.search; // => "?search=test" + var origin = parser.origin; // => "http://example.com:3000" + if (protocal === "http:" || protocal === "https:") { + return hostname !== ''; + } + return false; +} + +function getCookieByName(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +function isCSRFSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +} + +$.ajaxSetup({ + beforeSend: function (xhr, settings) { + if (!isCSRFSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRF-Token", getCookieByName('csrf_token')); + } + } +}); \ No newline at end of file diff --git a/ucenter.php b/ucenter.php new file mode 100755 index 0000000..95b7dd4 --- /dev/null +++ b/ucenter.php @@ -0,0 +1,213 @@ +Agents'), + array('clusters', '->Virtual Clusters'), + array('logs_all', '->Audit') +); +$visible_entries = array(); +foreach ($entries as $entry) { + if (AccessController::hasAccess(Session::get('role', 'visitor'), 'ucenter.' . $entry[0])) { + $visible_entries[] = array($entry[0], $entry[1]); + } +} +?> + + + + + Management | YAO + + + + + +
+ + +
+
+ + + +
+
+
+
Menu Bar
+ +
+
+ + +
+
+
Welcome page
+
+ Welcome back, .
+ Now:   +
+
+
+
Notice
+
+

Notices

+

Nothing new here.

+
+
+
+ + +
+
+
Job
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+
Resources
+
+
+
+ +
+
+
+
+
+ + +
+
+
日志
+
+
+
+ +
+
+
+
+
+ + +
+
+
Agents
+
+
+
+ +
+ +
+
+
+
+
+ + + +
+
+
+ + +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/user.logic.php b/user.logic.php new file mode 100644 index 0000000..dc41c4d --- /dev/null +++ b/user.logic.php @@ -0,0 +1,70 @@ +get('open_id')); + if ($res['user'] === null) { + /* if user(UID=1) not exist, set this user as admin + * WARN: do not delete user(UID=1) */ + if (UserManager::getByUID(1) === null) { + $info->set('role', 'admin'); + } + if (!UserManager::add($info)) { + $res['errno'] = Code::FAIL; + return $res; + } + $res['user'] = UserManager::getByOpenID($info->get('open_id')); + } + $res['errno'] = $res['user'] !== null ? Code::SUCCESS : Code::UNKNOWN_ERROR; + return $res; +} + +function user_signout() +{ + Session::expire(); + $res['errno'] = Code::SUCCESS; + return $res; +} + +function log_gets(CRObject $rule) +{ + if (Session::get('uid') === null) { + $res['errno'] = Code::NOT_LOGED; + return $res; + } + if ($rule->get('who') !== 'all') { + $rule->set('who', 'self'); + $rule->set('scope', Session::get('uid')); + $rule->set('tag', 'user.login'); + } + if ($rule->get('who') === 'all' && !AccessController::hasAccess(Session::get('role', 'visitor'), 'logs.get_others')) { + $res['errno'] = Code::NO_PRIVILEGE; + return $res; + } + $res['errno'] = Code::SUCCESS; + $res['count'] = CRLogger::getCount($rule); + $res['logs'] = CRLogger::search($rule); + return $res; +} + +function oauth_get_url() +{ + $state = Random::randomString(16); + Session::put('oauth:state', $state); + $url = OAUTH_SITE . '/login?response_type=code&client_id=' . OAUTH_CLIENT_ID . '&redirect_uri=' . BASE_URL . '/auth&state=' . $state . '&scope=role'; + $res['errno'] = Code::SUCCESS; + $res['url'] = $url; + return $res; +} \ No newline at end of file diff --git a/util4p/AccessController.class.php b/util4p/AccessController.class.php new file mode 100755 index 0000000..c8b4579 --- /dev/null +++ b/util4p/AccessController.class.php @@ -0,0 +1,34 @@ + array('Admin', 'Moderator', 'User'), + * 'post.comment' => array'Admin', 'Moderator', 'User'), + * 'post.pin' => array('Admin', 'Moderator'), + * 'user.block' => array('Admin') + * ); + */ + public static function setMap(array $map) + { + if (is_array($map)) { + self::$rules_array = $map; + } + } + + /* + * AccessController::hasAccess('Moderator', 'user.block'); + */ + public static function hasAccess($role, $operation) + { + if (array_key_exists($operation, self::$rules_array)) { + return in_array($role, self::$rules_array[$operation]); + } + return false; + } + +} diff --git a/util4p/CRLogger.class.php b/util4p/CRLogger.class.php new file mode 100755 index 0000000..428e9c6 --- /dev/null +++ b/util4p/CRLogger.class.php @@ -0,0 +1,165 @@ +get('db_table', self::$db_table); + } + + public static function log(CRObject $log) + { + $scope = $log->get('scope', ''); + $tag = $log->get('tag'); + $level = $log->getInt('level', self::LEVEL_INFO); + $ip = $log->get('ip', cr_get_client_ip(false)); + $time = $log->getInt('time', time()); + $content = $log->get('content'); + + $key_values = array( + 'scope' => '?', 'tag' => '?', 'level' => '?', 'ip' => '?', 'time' => '?', 'content' => '?' + ); + $builder = new SQLBuilder(); + $builder->insert(self::$db_table, $key_values); + $sql = $builder->build(); + $params = array($scope, $tag, $level, ip2long($ip), $time, $content); + return (new MysqlPDO())->execute($sql, $params); + } + + public static function search(CRObject $filter) + { + $scope = $filter->get('scope'); + $tag = $filter->get('tag'); + $level_min = $filter->getInt('level_min'); + $ip = $filter->get('ip'); + $time_begin = $filter->getInt('time_begin'); + $time_end = $filter->getInt('time_end'); + $offset = $filter->getInt('offset', 0); + $limit = $filter->getInt('limit', -1); + $order = $filter->get('order'); + + $selected_rows = array('id', 'scope', 'tag', 'level', 'ip', 'time', 'content'); + $where_arr = array(); + $opt_arr = array(); + $order_arr = array(); + $params = array(); + + if (!empty($scope)) { + $where_arr['scope'] = '?'; + $params[] = $scope; + } + if (!empty($tag)) { + $where_arr['tag'] = '?'; + $params[] = $tag; + } + if (!is_null($level_min)) { + $where_arr['level'] = '?'; + $opt_arr['level'] = '>='; + $params[] = $level_min; + } + if (!empty($ip)) { + $where_arr['ip'] = '?'; + $params[] = ip2long($ip); + } + if (!is_null($time_begin) && !is_null($time_end)) { + $where_arr['time'] = '? AND ?'; + $opt_arr['time'] = 'BETWEEN'; + $params[] = $time_begin; + $params[] = $time_end; + } else if (!is_null($time_begin)) { + $where_arr['time'] = '?'; + $opt_arr['time'] = '>='; + $params[] = $time_begin; + } else if (!is_null($time_end)) { + $where_arr['time'] = '?'; + $opt_arr['time'] = '<='; + $params[] = $time_end; + } + + switch ($order) { + case 'latest': + $order_arr['time'] = 'desc'; + break; + default: + $order_arr['id'] = 'desc'; + break; + } + $builder = new SQLBuilder(); + $builder->select(self::$db_table, $selected_rows); + $builder->where($where_arr, $opt_arr); + $builder->order($order_arr); + $builder->limit($offset, $limit); + $sql = $builder->build(); + $logs = (new MysqlPDO())->executeQuery($sql, $params); + return $logs; + } + + public static function getCount(CRObject $filter) + { + $scope = $filter->get('scope'); + $tag = $filter->get('tag'); + $level_min = $filter->getInt('level_min'); + $ip = $filter->get('ip'); + $time_begin = $filter->getInt('time_begin'); + $time_end = $filter->getInt('time_end'); + + $selected_rows = array('id'); + $where_arr = array(); + $opt_arr = array(); + $params = array(); + + if (!empty($scope)) { + $where_arr['scope'] = '?'; + $params[] = $scope; + } + if (!empty($tag)) { + $where_arr['tag'] = '?'; + $params[] = $tag; + } + if (!is_null($level_min)) { + $where_arr['level'] = '?'; + $opt_arr['level'] = '>='; + $params[] = $level_min; + } + if (!empty($ip)) { + $where_arr['ip'] = '?'; + $params[] = ip2long($ip); + } + if (!is_null($time_begin) && !is_null($time_end)) { + $where_arr['time'] = '? AND ?'; + $opt_arr['time'] = 'BETWEEN'; + $params[] = $time_begin; + $params[] = $time_end; + } else if (!is_null($time_begin)) { + $where_arr['time'] = '?'; + $opt_arr['time'] = '>='; + $params[] = $time_begin; + } else if (!is_null($time_end)) { + $where_arr['time'] = '?'; + $opt_arr['time'] = '<='; + $params[] = $time_end; + } + + $builder = new SQLBuilder(); + $builder->select(self::$db_table, $selected_rows); + $builder->where($where_arr, $opt_arr); + $builder->limit(0, 1000); + $sql = $builder->build(); + $sql = "SELECT COUNT(1) AS `count` FROM ( $sql ) as tmp"; + $res = (new MysqlPDO())->executeQuery($sql, $params); + return intval($res[0]['count']); + } + +} diff --git a/util4p/CRObject.class.php b/util4p/CRObject.class.php new file mode 100755 index 0000000..c3d42bb --- /dev/null +++ b/util4p/CRObject.class.php @@ -0,0 +1,69 @@ +map = $map; + } + + public function set($key, $value) + { + $this->map[$key] = $value; + return true; + } + + public function get($key, $default = null) + { + if (isset($this->map[$key]) && !is_null($this->map[$key])) { + return $this->map[$key]; + } + return $default; + } + + public function getInt($key, $default = null) + { + if (isset($this->map[$key]) && !is_null($this->map[$key]) && is_numeric($this->map[$key])) { + return intval($this->map[$key]); + } + return $default; + } + + public function getBool($key, $default = false) + { + if (isset($this->map[$key]) && !is_null($this->map[$key])) { + return $this->map[$key] === true; + } + return $default === true; + } + + public function toArray() + { + return $this->map; + } + + /* + * set $this[$key] if isset($obj[$key])&&!isset($this[$key]) + * (new CRObject(['k1' => 'v1'])).union(new CRObject(['k1' => 'v1.1', 'k2' => 'v2'])) + * = new CRObject(['k1' => 'v1', 'k2' => 'v2']) + */ + public function union(CRObject $obj) + { + $keys = array_keys($obj->toArray()); + foreach ($keys as $key) { + $this->set($key, $this->get($key, $obj->get($key))); + } + return $this; + } + + public function jsonSerialize() + { + return $this->toArray(); + } + +} diff --git a/util4p/LICENSE b/util4p/LICENSE new file mode 100755 index 0000000..8dada3e --- /dev/null +++ b/util4p/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/util4p/MysqlPDO.class.php b/util4p/MysqlPDO.class.php new file mode 100755 index 0000000..a03d496 --- /dev/null +++ b/util4p/MysqlPDO.class.php @@ -0,0 +1,99 @@ +get('host', self::$host); + self::$port = $config->getInt('port', self::$port); + self::$db = $config->get('db', self::$db); + self::$user = $config->get('user', self::$user); + self::$password = $config->get('password', self::$password); + self::$charset = $config->get('charset', self::$charset); + self::$show_error = $config->getBool('show_error', self::$show_error); + } + + /**/ + public function __construct() + { + $this->connect(); + } + + /**/ + private function connect() + { + try { + $this->dbh = new PDO('mysql:host=' . (self::$host) . ';port=' . (self::$port) . ';dbname=' . (self::$db), self::$user, self::$password); + $this->dbh->exec('SET names ' . self::$charset); + return true; + } catch (PDOException $e) { + $this->dbh = null; + if (self::$show_error) + var_dump($e->getMessage()); + return false; + } + } + + /* + * @return bool + * */ + public function execute($sql, $a_params, $need_inserted_id = false) + { + if ($this->dbh === null) { + return false; + } + try { + $stmt = $this->dbh->prepare($sql); + $result = $stmt->execute($a_params); + if (self::$show_error && !$result) { + var_dump($stmt->errorInfo()[2]); + } + if ($need_inserted_id) { + $result = $result ? $this->dbh->lastInsertId() : false; + } + $this->dbh = null; + return $result; + } catch (Exception $e) { + if (self::$show_error) + var_dump($e->getMessage()); + return false; + } + } + + /* + * @param $a_params: key-value array + */ + function executeQuery($sql, $a_params) + { + if ($this->dbh === null) { + return null; + } + try { + $stmt = $this->dbh->prepare($sql); + $stmt->setFetchMode(PDO::FETCH_ASSOC); + $result = null; + if ($stmt->execute($a_params)) { + $result = $stmt->fetchAll(); + } + $this->dbh = null; + return $result; + } catch (Exception $e) { + if (self::$show_error) + var_dump($e->getMessage()); + return null; + } + } + +} diff --git a/util4p/README.md b/util4p/README.md new file mode 100755 index 0000000..1735d70 --- /dev/null +++ b/util4p/README.md @@ -0,0 +1,23 @@ +# utilities for php + +Require PHP version >= 5.1.0 + +## AccessController.class.php + +## CRLogger.class.php + +## CRObject.class.php + +## MysqlPDO.class.php + +## Random.class.php + +## RedisDAO.class.php + +## Session.class.php + +## SQLBuilder.class.php + +## util.php + +## Validator.class.php diff --git a/util4p/Random.class.php b/util4p/Random.class.php new file mode 100755 index 0000000..78ac140 --- /dev/null +++ b/util4p/Random.class.php @@ -0,0 +1,51 @@ += $range); + return $min + $rnd; + } + + /* + * generate random string of length $length + * level: LOW - only numbers, MIDDLE - plus letters(upper and lower), HIGH - plus special chars + */ + public static function randomString($strlen, $level = self::LEVEL_MIDDLE) + { + $alphabet = '0123456789'; + if ($level > self::LEVEL_LOW) { + $alphabet .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $alphabet .= 'abcdefghijklmnopqrstuvwxyz'; + } + if ($level > self::LEVEL_MIDDLE) + $alphabet .= '+-*/?!%`~@#^&(){}'; + + $length = strlen($alphabet); + $token = ''; + for ($i = 0; $i < $strlen; $i++) { + $token .= $alphabet[self::randomInt(0, $length - 1)]; + } + return $token; + } + +} diff --git a/util4p/RateLimiter.class.php b/util4p/RateLimiter.class.php new file mode 100755 index 0000000..333fdd5 --- /dev/null +++ b/util4p/RateLimiter.class.php @@ -0,0 +1,169 @@ +100, //min point to reach this punishment + * 'interval'=>300 //count interval, degree will expire after interval + * ) + * + * rules are stored in redis db + * will auto grow by fatigue degree, call setAutoIncrease($bool) + * punish directly, call punish($arr) + */ + +class RateLimiter +{ + private static $keyPrefix = 'rl'; + private static $id = ''; + private static $rules = array(); + + + /* + * @param $key customize your own key, default is ip2long(ip) + */ + public static function configure(CRObject $config) + { + self::$keyPrefix = $config->get('key_prefix', self::$keyPrefix); + self::$id = $config->get('id', cr_get_client_ip(false)); + self::$rules = $config->get('rules', self::$rules); + } + + + /* + * @param + * + */ + public static function increase($degree) + { + if (!is_numeric($degree)) { + return false; + } + $lua_script = <<eval($lua_script, 1, $key, $degree, $interval); + } + $redis->disconnect(); + $rule = self::whichRuleToPunish(); + if ($rule !== null) { + self::punish($rule); + } + return true; + } + + + /**/ + public static function punish($rule, $id = null) + { + if ($id === null) { + $id = self::$id; + } + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $redis->set(self::$keyPrefix . ':punishing:' . $id, $rule['degree'], 'EX', $rule['interval']); + $redis->disconnect(); + return true; + } + + + /* + * get punish time left, negative means not being punished + */ + public static function getFreezeTime($id = null) + { + if ($id === null) { + $id = self::$id; + } + $redis = RedisDAO::instance(); + if ($redis === null) { + return 0; + } + $freezeTime = (int)$redis->ttl(self::$keyPrefix . ':punishing:' . $id); + $redis->disconnect(); + return $freezeTime; + } + + /* + * get which rule to punish current user + * mostly of the time, you dont have to call this, as it is called automatically + */ + private static function whichRuleToPunish() + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return null; + } + foreach (self::$rules as $rule) { + $interval = $rule['interval']; + $key = self::$keyPrefix . ':degree:' . self::$id . '-' . $interval; + $degree = (int)$redis->get($key); + if ($degree > $rule['degree']) { + $redis->disconnect(); + return $rule; + } + } + $redis->disconnect(); + return null; + } + + /* clear degree count and punishing state */ + public static function clear($id = null) + { + if ($id === null) { + $id = self::$id; + } + $redis = RedisDAO::instance(); + if ($redis === null) { + return null; + } + foreach (self::$rules as $rule) { + $interval = $rule['interval']; + $key = self::$keyPrefix . ':degree:' . $id . '-' . $interval; + $redis->del(array($key)); + } + $redis->del(array(self::$keyPrefix . ':punishing:' . $id)); + $redis->disconnect(); + return true; + } + + /* list IDs being punished */ + public static function listPunished() + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $redis_key = self::$keyPrefix . ':punishing:*'; + $list = $redis->keys($redis_key); + $redis->disconnect(); + $len = strlen(self::$keyPrefix . ':punishing:'); + $ids = array(); + foreach ($list as $item) { + $ids[]['id'] = mb_substr($item, $len); + } + return $ids; + } + +} diff --git a/util4p/ReSession.class.php b/util4p/ReSession.class.php new file mode 100755 index 0000000..beab120 --- /dev/null +++ b/util4p/ReSession.class.php @@ -0,0 +1,208 @@ +get('time_out', self::$time_out); + self::$bind_ip = $config->getBool('bind_ip', self::$bind_ip); + self::$guid_key = $config->get('guid_key', self::$guid_key); + /* assign id from new sessions */ + if (!isset($_COOKIE[self::$guid_key])) { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + do { // generate an unique session id + self::$sid = Random::randomString(64); + } while ($redis->exists('session:' . self::$sid) === 1); + $redis->disconnect(); + setcookie(self::$guid_key, self::$sid); + } else { + self::$sid = $_COOKIE[self::$guid_key]; + } + return true; + } + + /* Ask browser to remember session id even if browser restarts */ + public static function persist($duration) + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $redis_key = 'session:' . self::$sid; + $redis->expire($redis_key, $duration); + $redis->disconnect(); + setcookie(self::$guid_key, self::$sid, time() + $duration); + return true; + } + + /* attach session to $group to avoid wild sessions */ + public static function attach($group) + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $redis_key = 'session:' . self::$sid; + $key = 'session-group:' . $group; + $redis->sadd($key, array($redis_key)); + $redis->disconnect(); + return true; + } + + /* detach session from $group */ + public static function detach($group) + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $key = 'session-group:' . $group; + $redis_key = 'session:' . self::$sid; + $redis->srem($key, $redis_key); + $redis->disconnect(); + return true; + } + + /**/ + public static function put($key, $value) + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $redis_key = 'session:' . self::$sid; + $redis->hset($redis_key, $key, $value); + $redis->hset($redis_key, '_ip', cr_get_client_ip(false)); + self::$cache[$key] = $value;//update cache + self::get('_ip');//renew expiration + $redis->disconnect(); + return true; + } + + /**/ + public static function get($key, $default = null) + { + if (isset(self::$cache[$key])) { + return self::$cache[$key]; + } + $redis = RedisDAO::instance(); + if ($redis === null) { + return $default; + } + $redis_key = 'session:' . self::$sid; + $list = $redis->hgetall($redis_key); + if (self::$bind_ip) { + if (!(isset($list['_ip']) && $list['_ip'] === cr_get_client_ip(false))) { + return $default; + } + } + self::$cache = $list; + if ($redis->ttl($redis_key) < self::$time_out) { + $redis->expire($redis_key, self::$time_out); + } + $redis->disconnect(); + if (isset($list[$key])) { + return $list[$key]; + } + return $default; + } + + /* expire current session */ + public static function expire() + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $redis_key = 'session:' . self::$sid; + $redis->del(array($redis_key)); + $redis->disconnect(); + setcookie(self::$guid_key, self::$sid, time() - 3600); + return true; + } + + /**/ + public static function expireByGroup($group, $index = null) + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $group_key = "session-group:$group"; + $keys = $redis->smembers($group_key); + foreach ($keys as $i => $key) { + if ($index === null || $i === $index) { + $redis->srem($group_key, $key); + $redis->del($key); + } + } + $redis->disconnect(); + return true; + } + + /* Low Performance, Not recommended */ + public static function listGroup(CRObject $rule) + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $prefix = 'session-group:'; + $keys = $redis->keys($prefix . '*'); + $offset = $rule->getInt('offset', 0); + $limit = $rule->getInt('limit', -1); + $groups = array(); + $len = strlen($prefix); + foreach ($keys as $index => $key) { + if ($index < $offset) { + continue; + } + if ($limit !== -1 && $offset + $limit <= $index) { + break; + } + $groups[] = array('index' => $index, 'group' => substr($key, $len)); + } + $redis->disconnect(); + return $groups; + } + + /* Low Performance, Not recommended */ + public static function listSession(CRObject $rule) + { + $redis = RedisDAO::instance(); + if ($redis === null) { + return false; + } + $group = $rule->get('group', ''); + $redis_key = "session-group:$group"; + $keys = $redis->smembers($redis_key); + $sessions = array(); + foreach ($keys as $index => $key) { + $session = $redis->hgetall($key); + if (count($session) === 0) { + $redis->srem($redis_key, $key); + continue; + } + $session['_index'] = $index; + $session['_current'] = $key === 'session:' . self::$sid; + $sessions[] = $session; + } + return $sessions; + } + +} diff --git a/util4p/RedisDAO.class.php b/util4p/RedisDAO.class.php new file mode 100755 index 0000000..9931628 --- /dev/null +++ b/util4p/RedisDAO.class.php @@ -0,0 +1,44 @@ +get('scheme', self::$scheme); + self::$host = $config->get('host', self::$host); + self::$port = $config->getInt('port', self::$port); + self::$show_error = $config->getBool('show_error', self::$show_error); + } + + public static function instance() + { + try { + $redis = new Predis\Client( + array( + 'scheme' => RedisDAO::$scheme, + 'host' => RedisDAO::$host, + 'port' => RedisDAO::$port + ) + ); + $redis->connect(); + return $redis; + } catch (Exception $e) { + if (self::$show_error) + var_dump($e->getMessage()); + return null; + } + } + +} diff --git a/util4p/SQLBuilder.class.php b/util4p/SQLBuilder.class.php new file mode 100755 index 0000000..206a199 --- /dev/null +++ b/util4p/SQLBuilder.class.php @@ -0,0 +1,162 @@ +sql = "INSERT INTO `$table`"; + $keys = array_keys($key_values); + if (count($keys) && $keys[0] !== 0) { //support `INSERT INTO table VALUES (...) + $this->sql .= '('; + foreach ($keys as $key) { + $this->sql .= "`$key`, "; + } + $this->sql = substr($this->sql, 0, strlen($this->sql) - 2); + $this->sql .= ')'; + } + $this->sql .= " VALUES("; + foreach ($key_values as $key => $value) { + if ($value === null) { + $this->sql .= "null, "; + } else if ($value === '?') {// support prepared statment + $this->sql .= "?, "; + } else {// FIXME: $value may contains ' + $this->sql .= "'$value', "; + } + } + $this->sql = substr($this->sql, 0, strlen($this->sql) - 2); + $this->sql .= ")"; + } + + /**/ + public function select($table, array $selected_rows = array()) + { + $this->sql = 'SELECT '; + foreach ($selected_rows as $row) { + if (strpos($row, ' ') || strpos($row, '(')) {//contain functions, not pure row name + $this->sql .= "$row, "; + } else { + $this->sql .= "`$row`, "; + } + } + if (count($selected_rows) === 0) { + $this->sql .= ' * '; + } else { + $this->sql = substr($this->sql, 0, strlen($this->sql) - 2); + } + $this->sql .= " FROM `$table` "; + } + + /**/ + public function update($table, array $key_values) + { + if ($key_values === null || count($key_values) === 0) { + return; + } + $this->sql = "UPDATE `$table` SET "; + foreach ($key_values as $key => $value) { + if ($value === null) { + $this->sql .= " `$key` = null, "; + } else if ($value === '?') { + $this->sql .= " `$key` = ?, "; + } else {//FIXME: $value may contain ' + $this->sql .= " `$key` = '$value', "; + } + } + $this->sql = substr($this->sql, 0, strlen($this->sql) - 2); + } + + /**/ + public function delete($table) + { + $this->sql = "DELETE FROM `$table` "; + } + + /* currently, can only represent AND */ + public function where($key_values, $opts = array()) + { + $where_clause_cnt = 0; + $keys = array_keys($key_values); + + foreach ($keys as $key) { + if (!isset($opts[$key])) { + $opts[$key] = '='; + } + if ($where_clause_cnt == 0) { + $this->sql .= ' WHERE '; + } else { + $this->sql .= ' AND '; + } + if ($key_values[$key] === null) { + if ($opts[$key] === '=') { + $this->sql .= " `$key` is null "; + } else { + $this->sql .= " `$key` is not null "; + } + $where_clause_cnt++; + } else if ($key_values[$key] === '?' || in_array(strtoupper($opts[$key]), array('IN', 'BETWEEN', 'LIKE'))) { + $this->sql .= " `$key` {$opts[$key]} {$key_values[$key]} "; + $where_clause_cnt++; + } else {//FIXME: $key_values[$key] may contain ' + $this->sql .= " `$key` {$opts[$key]} '{$key_values[$key]}' "; + $where_clause_cnt++; + } + } + } + + /**/ + public function group(array $by_arr) + { + if ($by_arr === null || count($by_arr) === 0) { + return; + } + $this->sql .= ' GROUP BY '; + foreach ($by_arr as $by) { + $this->sql .= "`$by`, "; + } + $this->sql = substr($this->sql, 0, strlen($this->sql) - 2); + } + + /**/ + public function order(array $by_arr) + { + if ($by_arr === null || count($by_arr) === 0) { + return; + } + $this->sql .= ' ORDER BY '; + foreach ($by_arr as $by => $order) { + if (strtoupper($order) !== 'DESC') { + $order = 'ASC'; + } + $this->sql .= "`$by` $order, "; + } + $this->sql = substr($this->sql, 0, strlen($this->sql) - 2); + } + + /**/ + public function limit($offset, $cnt) + { + if ($cnt < 0) {// support LIMIT 0 ,-1 to get all records + $this->sql .= " LIMIT $offset, 18446744073709551615"; + } else { + $this->sql .= " LIMIT $offset, $cnt"; + } + } + + /**/ + public function build() + { + return $this->sql; + } + +} diff --git a/util4p/Session.class.php b/util4p/Session.class.php new file mode 100755 index 0000000..b368d4f --- /dev/null +++ b/util4p/Session.class.php @@ -0,0 +1,50 @@ +get('time_out', self::$time_out); + self::$bind_ip = $config->getBool('bind_ip', self::$bind_ip); + return true; + } + + /**/ + public static function put($key, $value) + { + $_SESSION[$key] = $value; + $_SESSION['_SELF']['LAST_ACTIVE'] = time(); + return true; + } + + /**/ + public static function get($key, $default = null) + { + if (!isset($_SESSION['_SELF']['LAST_ACTIVE'])) { + $_SESSION['_SELF']['LAST_ACTIVE'] = 0; + } + if (self::$time_out > 0 && time() - $_SESSION['_SELF']['LAST_ACTIVE'] > self::$time_out) { + return $default; + } + $_SESSION['_SELF']['LAST_ACTIVE'] = time(); + if (isset($_SESSION[$key]) && !is_null($_SESSION[$key])) { + return $_SESSION[$key]; + } + return $default; + } + + /* expire current session */ + public static function expire() + { + $_SESSION = array(); + session_destroy(); + return true; + } + +} diff --git a/util4p/Validator.class.php b/util4p/Validator.class.php new file mode 100755 index 0000000..d49cb87 --- /dev/null +++ b/util4p/Validator.class.php @@ -0,0 +1,36 @@ + 255) { + return false; + } + } + return preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $str); + } + + /**/ + public static function isEmail($str) + { + if ($str === null) { + return false; + } + return preg_match("/^([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)$/i", $str) === 1; + } + + + /*TODO*/ + public static function isURL($url) + { + if (is_null($url) || empty($url)) { + return false; + } + return true; + } + +} diff --git a/util4p/util.php b/util4p/util.php new file mode 100755 index 0000000..0e05bf9 --- /dev/null +++ b/util4p/util.php @@ -0,0 +1,67 @@ + 0) { + return $_GET[$key]; + } + return $default; +} + +/* get from $_POST */ +function cr_get_POST($key, $default = null) +{ + if (isset($_POST[$key]) && strlen($_POST[$key]) > 0) { + return $_POST[$key]; + } + return $default; +} + +/* get from $_COOKIE */ +function cr_get_COOKIE($key, $default = null) +{ + if (isset($_COOKIE[$key]) && strlen($_COOKIE[$key]) > 0) { + return $_COOKIE[$key]; + } + return $default; +} + +/* get from $_SERVER */ +function cr_get_SERVER($key, $default = null) +{ + if (isset($_SERVER[$key]) && strlen($_SERVER[$key]) > 0) { + return $_SERVER[$key]; + } + return $default; +} \ No newline at end of file