1
0
mirror of https://github.com/newnius/YAO-portal.git synced 2025-06-06 07:11:54 +00:00

init & add agent & add job

This commit is contained in:
Newnius 2019-01-15 10:02:28 +08:00
parent 71f1f10e2c
commit d0a4b891b5
321 changed files with 24657 additions and 1 deletions

7
.htaccess Executable file
View File

@ -0,0 +1,7 @@
RewriteEngine on
RewriteRule ^service$ /ajax.php
RewriteRule ^auth$ /auth.php
RewriteRule ^help$ /help.php
RewriteRule ^ucenter$ /ucenter.php

27
404.php Normal file
View File

@ -0,0 +1,27 @@
<?php
require_once('Code.class.php');
require_once('secure.inc.php');
$error = '404 Not Found';
?>
<!DOCTYPE html>
<html lang="en-US">
<head>
<?php require_once('head.php'); ?>
<title>404 | YAO</title>
</head>
<body>
<div class="wrapper">
<?php require_once('header.php'); ?>
<div class="container">
<div class="container">
<h2 style="text-align: center"><?= $error ?></h2>
</div>
</div> <!-- /container -->
<!--This div exists to avoid footer from covering main body-->
<div class="push"></div>
</div>
<?php require_once('footer.php'); ?>
</body>
</html>

92
AgentManager.class.php Executable file
View File

@ -0,0 +1,92 @@
<?php
require_once('util4p/CRObject.class.php');
require_once('util4p/MysqlPDO.class.php');
require_once('util4p/SQLBuilder.class.php');
class AgentManager
{
private static $table = 'yao_agent';
/*
* do add link
*/
public static function add(CRObject $agent)
{
$ip = $agent->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);
}
}

3
CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
# Changelog
## 0.0.1-alpha (2019-01-08)

73
Code.class.php Executable file
View File

@ -0,0 +1,73 @@
<?php
class Code
{
/* common */
const SUCCESS = 0;
const FAIL = 1;
const NO_PRIVILEGE = 2;
const UNKNOWN_ERROR = 3;
const IN_DEVELOP = 4;
const INVALID_REQUEST = 5;
const UNKNOWN_REQUEST = 6;
const CAN_NOT_BE_EMPTY = 7;
const INCOMPLETE_CONTENT = 8;
const FILE_NOT_UPLOADED = 9;
const RECORD_NOT_EXIST = 10;
const RECORD_ALREADY_EXIST = 34;
const INVALID_PASSWORD = 11;
const UNABLE_TO_CONNECT_REDIS = 12;
const UNABLE_TO_CONNECT_MYSQL = 13;
/* user */
const USERNAME_OCCUPIED = 14;
const EMAIL_OCCUPIED = 15;
const INVALID_USERNAME = 16;
const INVALID_EMAIL = 17;
const WRONG_PASSWORD = 18;
const NOT_LOGED = 19;
const USER_NOT_EXIST = 20;
const USER_IS_BLOCKED = 21;
const USER_IS_REMOVED = 22;
const EMAIL_IS_NOT_VERIFIED = 23;
const USERNAME_MISMATCH_EMAIL = 24;
const CODE_EXPIRED = 25;
const EMAIL_ALREADY_VERIFIED = 26;
const INVALID_COOKIE = 27;
/* site */
const INVALID_DOMAIN = 28;
const NEED_VERIFY = 29;
const INVALID_PATTERN = 30;
/* auth */
const TOKEN_EXPIRED = 31;
const SITE_NOT_EXIST = 32;
const INVALID_URL = 33;
const INVALID_PARAM = 34;
const DOMAIN_MISMATCH = 35;
const TOKEN_LENGTH_INVALID = 36;
const URL_LENGTH_INVALID = 37;
const RECORD_PAUSED = 38;
const RECORD_REMOVED = 39;
const RECORD_DISABLED = 40;
const RECORD_NOT_IN_VALID_TIME = 41;
/* rate limit */
const TOO_FAST = 30;
public static function getErrorMsg($errno)
{
switch ($errno) {
case Code::SUCCESS:
return 'Success !';
default:
return 'Unknown Error Code(' . $errno . ') !';
}
}
}

127
JobManager.class.php Executable file
View File

@ -0,0 +1,127 @@
<?php
require_once('util4p/CRObject.class.php');
require_once('util4p/MysqlPDO.class.php');
require_once('util4p/SQLBuilder.class.php');
class JobManager
{
private static $table = 'yao_job';
/*
* do submit job
*/
public static function add(CRObject $job)
{
$name = $job->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);
}
}

View File

@ -1 +1 @@
# YAO-portal # YAO-Portal

36
Securer.class.php Executable file
View File

@ -0,0 +1,36 @@
<?php
require_once('util4p/util.php');
require_once('util4p/CRObject.class.php');
require_once('util4p/Random.class.php');
class Securer
{
/* configuration && initialization */
public static function configure(CRObject $config)
{
}
/**/
public static function set_csrf_token()
{
if (!isset($_COOKIE['csrf_token'])) {
setcookie('csrf_token', Random::randomString(32));
}
}
/**/
public static function validate_csrf_token()
{
$csrf_token = null;
if (isset($_SERVER['HTTP_X_CSRF_TOKEN'])) {
$csrf_token = $_SERVER['HTTP_X_CSRF_TOKEN'];
}
$success = $csrf_token !== null && isset($_COOKIE['csrf_token']) && $csrf_token === $_COOKIE['csrf_token'];
/* whatever, refresh csrf_token to expire current token */
setcookie('csrf_token', Random::randomString(32));
return $success;
}
}

129
Spider.class.php Normal file
View File

@ -0,0 +1,129 @@
<?php
require_once('util4p/CRObject.class.php');
class Spider
{
private $userAgent = 'Spider';
private $accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8';
private $acceptEncoding = 'gzip, deflate, br';
private $acceptLanguage = 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7';
private $cookie = '';
private $referer = '';
private $timeout = 15;
private $headers = array();
private $body = '';
private $info = array();
public function configure(CRObject $config)
{
$this->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'];
}
}

57
UserManager.class.php Normal file
View File

@ -0,0 +1,57 @@
<?php
require_once('util4p/util.php');
require_once('util4p/CRObject.class.php');
require_once('util4p/SQLBuilder.class.php');
require_once('util4p/MysqlPDO.class.php');
require_once('util4p/Validator.class.php');
class UserManager
{
/**/
public static function add(CRObject $user)
{
$open_id = $user->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;
}
}

65
agent.logic.php Normal file
View File

@ -0,0 +1,65 @@
<?php
require_once('predis/autoload.php');
require_once('util4p/util.php');
require_once('util4p/CRObject.class.php');
require_once('util4p/Random.class.php');
require_once('util4p/AccessController.class.php');
require_once('util4p/CRLogger.class.php');
require_once('Code.class.php');
require_once('AgentManager.class.php');
require_once('config.inc.php');
require_once('init.inc.php');
function agent_add(CRObject $agent)
{
if (!AccessController::hasAccess(Session::get('role', 'visitor'), 'agent.add')) {
$res['errno'] = Code::NO_PRIVILEGE;
return $res;
}
if (AgentManager::getByIP($agent->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;
}

136
ajax.php Normal file
View File

@ -0,0 +1,136 @@
<?php
require_once('util4p/util.php');
require_once('util4p/CRObject.class.php');
require_once('Code.class.php');
require_once('Securer.class.php');
require_once('user.logic.php');
require_once('job.logic.php');
require_once('agent.logic.php');
require_once('config.inc.php');
require_once('init.inc.php');
function csrf_check($action)
{
/* check referer, just in case I forget to add the method to $post_methods */
$referer = cr_get_SERVER('HTTP_REFERER', '');
$url = parse_url($referer);
$host = isset($url['host']) ? $url['host'] : '';
$host .= isset($url['port']) && $url['port'] !== 80 ? ':' . $url['port'] : '';
if ($host !== cr_get_SERVER('HTTP_HOST')) {
return false;
}
$post_methods = array(
'job_submit',
'job_stop',
'signout',
'oauth_get_url'
);
if (in_array($action, $post_methods)) {
return Securer::validate_csrf_token();
}
return true;
}
function print_response($res)
{
if (!isset($res['msg']))
$res['msg'] = Code::getErrorMsg($res['errno']);
$json = json_encode($res);
header('Content-type: application/json');
echo $json;
}
$res = array('errno' => 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);

89
auth.php Executable file
View File

@ -0,0 +1,89 @@
<?php
require_once('predis/autoload.php');
require_once('util4p/util.php');
require_once('util4p/CRObject.class.php');
require_once('util4p/ReSession.class.php');
require_once('util4p/CRLogger.class.php');
require_once('Code.class.php');
require_once('Spider.class.php');
require_once('user.logic.php');
require_once('config.inc.php');
require_once('init.inc.php');
//check state
$state = cr_get_GET('state');
if ($state === null || $state !== Session::get('oauth:state')) {
echo 'Auth failed, state check failed!';
exit;
}
$client_id = OAUTH_CLIENT_ID;
$client_secret = OAUTH_CLIENT_SECRET;
$url = OAUTH_SITE . '/api?action=get_token';
$fields = array(
'grant_type' => '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;

52
config-sample.inc.php Executable file
View File

@ -0,0 +1,52 @@
<?php
define('YAO_VERSION', '0.2.1');
/* Custom */
/* Mysql */
/* It is not recommended to use `root` in production environment */
define('DB_HOST', 'localhost');
define('DB_PORT', 3306);
define('DB_NAME', 'yao');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
define('DB_SHOW_ERROR', false); // set to true to see detailed Mysql errors __only__ for debug purpose
/* Redis */
/* Make sure that your Redis only listens to Intranet */
define('REDIS_SCHEME', 'tcp');
define('REDIS_HOST', 'localhost');
define('REDIS_PORT', 6379);
define('REDIS_SHOW_ERROR', false); // set to true to see detailed Redis errors __only__ for debug purpose
/* Site */
define('BASE_URL', 'http://127.0.0.1'); // make absolute url for SEO and avoid hijack, no '/' at the end
define('WEB_ROOT', __DIR__);
define('FEEDBACK_EMAIL', 'support@newnius.com');
/* Auth */
define('AUTH_CODE_TIMEOUT', 300); // 5 min
define('AUTH_TOKEN_TIMEOUT', 604800); // 7 day
/* Session */
define('ENABLE_MULTIPLE_LOGIN', true);
define('BIND_SESSION_WITH_IP', false); // current session will be logged when ip changes
define('SESSION_TIME_OUT', 1800);// 30 minutes 30*60=1800
define('ENABLE_COOKIE', true);
/* Rate Limit */
define('ENABLE_RATE_LIMIT', false);
define('RATE_LIMIT_PREFIX', 'rl');
/* OAuth */
/* The default conf is only usable when this runs on localhost */
define('OAUTH_SITE', 'https://quickauth.newnius.com');
define('OAUTH_CLIENT_ID', 'XgaII6NxeE08LtKB');
define('OAUTH_CLIENT_SECRET', 'L9hdi4dQToM0GsDLtcYYQ3k4ZDEjuGVOtPS3nOVKlo6cxLcVjH9TqvmTBiHAgLp2');
header("content-type:text/html; charset=utf-8");
date_default_timezone_set('Asia/Shanghai');

23
footer.php Executable file
View File

@ -0,0 +1,23 @@
<?php
require_once('config.inc.php');
?>
<footer class="container">
<div class="footer">
<ul class="breadcrumb">
<li>&copy;2018&nbsp;<a href="<?= BASE_URL ?>/">YAO</a></li>
<li><a href="https://blog.newnius.com/" target="_blank">Newnius</a></li>
<li><a href="<?= BASE_URL ?>/help#TOS">TOS</a></li>
<li><a href="<?= BASE_URL ?>/help#privacy">Privacy</a></li>
<li><a href="<?= BASE_URL ?>/help#feedback">Feedback</a></li>
<li><a href="<?= BASE_URL ?>/help#about">About</a></li>
</ul>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/moment@2.22.2/min/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/eonasdan-bootstrap-datetimepicker@4.17.47/src/js/bootstrap-datetimepicker.min.js"></script>
<script src="static/config.js"></script>
<script src="static/util.js"></script>
<script src="static/script.js"></script>
<script async src="https://cdn.newnius.com/ana/ea.js"></script>

1
global.inc.php Normal file
View File

@ -0,0 +1 @@
<?php

12
head.php Normal file
View File

@ -0,0 +1,12 @@
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="keywords" content="Deep Learning, GPU resource management"/>
<meta name="description" content="Deep Learning, GPU resource management"/>
<meta name="author" content="Newnius"/>
<link rel="icon" href="static/favicon.ico"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet"/>
<link href="static/style.css" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/eonasdan-bootstrap-datetimepicker@4.17.47/build/css/bootstrap-datetimepicker.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js"></script>

40
header.php Executable file
View File

@ -0,0 +1,40 @@
<?php
require_once('predis/autoload.php');
require_once('util4p/ReSession.class.php');
require_once('config.inc.php');
require_once('init.inc.php');
?>
<header id="header" class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="<?= BASE_URL ?>">YAO</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<?php if (!Session::get('uid')) { ?>
<li><a href="javascript:void(0)" id="btn-oauth-login">Login</a></li>
<?php } else { ?>
<li><a href="<?= BASE_URL ?>/ucenter"><?= htmlspecialchars(Session::get('nickname')) ?></a></li>
<?php } ?>
<li class="dropdown">
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false">More<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="<?= BASE_URL ?>/help">Help</a></li>
<li role="separator" class="divider"></li>
<?php if (Session::get('uid')) { ?>
<li><a href="javascript:void(0)" id="btn-signout">Logout</a></li>
<?php } ?>
</ul>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container -->
</header>

84
help.php Executable file
View File

@ -0,0 +1,84 @@
<?php
require_once('config.inc.php');
require_once('secure.inc.php');
?>
<!DOCTYPE html>
<html lang="en-US">
<head>
<?php require('head.php'); ?>
<title>Help | YAO</title>
</head>
<body>
<div class="wrapper">
<?php require('header.php'); ?>
<?php require('modals.php'); ?>
<div class="container">
<div class="row">
<div class="col-sm-4 col-md-3 hidden-xs">
<div id="help-nav" class="panel panel-default">
<div class="panel-heading">List</div>
<ul class="nav nav-pills nav-stacked panel-body">
<li role="presentation">
<a href="#introduction">YAO</a>
</li>
<li role="presentation">
<a href="#about">About</a>
</li>
<li role="presentation">
<a href="#TOS">TOS</a>
</li>
<li role="presentation">
<a href="#privacy">Privacy</a>
</li>
<li role="presentation">
<a href="#feedback">Feedback</a>
</li>
</ul>
</div>
</div>
<div class="col-xs-12 col-sm-8 col-md-8 col-md-offset-1 ">
<div id="introduction" class="panel panel-default">
<div class="panel-heading">YAO</div>
<div class="panel-body">
<p>Yet Another Octopus</p>
</div>
</div>
<div id="about" class="panel panel-default">
<div class="panel-heading">About</div>
<div class="panel-body">
<ul>
<li>one</li>
<li>two</li>
</ul>
</div>
</div>
<div id="TOS" class="panel panel-default">
<div class="panel-heading">TOS</div>
<div class="panel-body">
<p>Term of service</p>
</div>
</div>
<div id="privacy" class="panel panel-default">
<div class="panel-heading">Privacy</div>
<div class="panel-body">
<p>Privacy</p>
</div>
</div>
<div id="feedback" class="panel panel-default">
<div class="panel-heading">Feedback</div>
<div class="panel-body">
<p>Contact with
<a href="mailto:<?= FEEDBACK_EMAIL ?>?subject=From LS"><?= FEEDBACK_EMAIL ?></a>
</p>
</div>
</div>
</div>
</div>
</div> <!-- /container -->
<!--This div exists to avoid footer from covering main body-->
<div class="push"></div>
</div>
<?php require('footer.php'); ?>
</body>
</html>

28
index.php Normal file
View File

@ -0,0 +1,28 @@
<?php
require_once('config.inc.php');
require_once('secure.inc.php');
?>
<!DOCTYPE html>
<html lang="en-US">
<head>
<?php require('head.php'); ?>
<title>Yet Another Octopus | YAO</title>
</head>
<body>
<div class="wrapper">
<?php require('header.php'); ?>
<?php require('modals.php'); ?>
<div class="container">
<div id="main" class="form ui-widget load-overlay container">
<h2>YAO-Yet Another Octopus</h2>
</div>
</div> <!-- /container -->
<!--This div exists to avoid footer from covering main body-->
<div class="push"></div>
</div>
<?php require('footer.php'); ?>
<script src="static/main.js"></script>
</body>
</html>

95
init.inc.php Normal file
View File

@ -0,0 +1,95 @@
<?php
require_once('predis/autoload.php');
require_once('util4p/MysqlPDO.class.php');
require_once('util4p/RedisDAO.class.php');
require_once('util4p/CRLogger.class.php');
require_once('util4p/ReSession.class.php');
require_once('util4p/CRObject.class.php');
require_once('util4p/AccessController.class.php');
require_once('config.inc.php');
init_mysql();
init_redis();
init_logger();
init_Session();
init_accessMap();
function init_mysql()
{
$config = new CRObject();
$config->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);
}

197
install.php Executable file
View File

@ -0,0 +1,197 @@
<?php
require_once('util4p/util.php');
require_once('util4p/CRObject.class.php');
require_once('util4p/MysqlPDO.class.php');
require_once('config.inc.php');
require_once('init.inc.php');
/* show error for debug purpose */
$config = new CRObject();
$config->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 ? '<em>Success</em>' : '<em>Failed</em>';
echo "<hr/>";
}
}
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);
}

95
job.logic.php Normal file
View File

@ -0,0 +1,95 @@
<?php
require_once('predis/autoload.php');
require_once('util4p/util.php');
require_once('util4p/CRObject.class.php');
require_once('util4p/AccessController.class.php');
require_once('util4p/CRLogger.class.php');
require_once('Code.class.php');
require_once('JobManager.class.php');
require_once('Spider.class.php');
require_once('config.inc.php');
require_once('init.inc.php');
function job_submit(CRObject $job)
{
if (!AccessController::hasAccess(Session::get('role', 'visitor'), 'job.submit')) {
$res['errno'] = Code::NO_PRIVILEGE;
return $res;
}
$job->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;
}

191
modals.php Executable file
View File

@ -0,0 +1,191 @@
<!-- msg modal -->
<div class="modal fade" id="modal-msg" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content panel-warning">
<div class="modal-header panel-heading">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 id="modal-msg-title" class="modal-title">Notice</h4>
</div>
<div class="modal-body">
<h4 id="modal-msg-content" class="text-msg text-center">Something is wrong!</h4>
</div>
</div>
</div>
</div>
<!-- job description modal -->
<div class="modal fade" id="modal-job-description" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content panel-info">
<div class="modal-header panel-heading">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Describe This Job</h4>
</div>
<div class="modal-body">
<pre id="modal-job-description-content"></pre>
</div>
</div>
</div>
</div>
<!-- job modal -->
<div class="modal fade" id="modal-job" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content panel-info">
<div class="modal-header panel-heading">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 id="modal-job-title" class="modal-title">Submit New Job</h4>
</div>
<div class="modal-body">
<form class="form" action="javascript:void(0)">
<label>Job Name</label>
<div class="form-group form-group-lg">
<label for="form-job-name" class="sr-only">Job Name</label>
<input type="text" id="form-job-name" class="form-control" maxlength="64"
placeholder="A readable job name" required/>
</div>
<label>Docker Image</label>
<div class="form-group form-group-lg">
<label for="form-job-image" class="sr-only">Docker Image</label>
<input type="text" id="form-job-image" class="form-control" maxlength="256"
placeholder="eg. yao/tensorflow:1.12" required/>
</div>
<label>Workspace</label>
<div class="form-group form-group-lg">
<label for="form-job-workspace" class="sr-only">Workspace</label>
<select id="form-job-workspace" class="form-control">
<option value="1">Workspace 1</option>
</select>
</div>
<label>Virtual Cluster</label>
<div class="form-group form-group-lg">
<label for="form-job-cluster" class="sr-only">Virtual Cluster</label>
<select id="form-job-cluster" class="form-control">
<option value="1">Cluster 1</option>
</select>
</div>
<label>Priority</label>
<div class="form-group form-group-lg">
<label for="form-job-priority" class="sr-only">Job Priority</label>
<select id="form-job-priority" class="form-control">
<option value="99">Urgent</option>
<option value="50">High</option>
<option value="25" selected>Medium</option>
<option value="1">Low</option>
</select>
</div>
<label>Run Before</label>
<div class="form-group form-group-lg">
<div class='input-group date date-picker'>
<label for="form-job-run-before" class="sr-only">Run Before</label>
<input type='text' class="form-control" placeholder="Run this job before"
id="form-job-run-before"
autocomplete="off"/>
<div class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</div>
</div>
</div>
<label>Tasks</label>
<div class="row" id="form-job-tasks">
<div class="col-md-2">
<label>Name</label>
<div class="form-group">
<input type="text" class="form-control" maxlength="32"
placeholder="Task Name & Node Name" required/>
</div>
</div>
<div class="col-md-2">
<label>CMD</label>
<div class="form-group">
<input type="text" class="form-control" maxlength="255"
placeholder="Command to bring up task" required/>
</div>
</div>
<div class="col-md-2">
<label>CPU Number</label>
<div class="form-group">
<input type="number" class="form-control" step="1" min="1"
placeholder="number of CPU required" required/>
</div>
</div>
<div class="col-md-2">
<label>Memory</label>
<div class="form-group">
<input type="number" class="form-control" step="512" min="512"
placeholder="MB" required/>
</div>
</div>
<div class="col-md-2">
<label>GPU Number</label>
<div class="form-group">
<input type="number" class="form-control" step="1" min="1"
placeholder="number of GPU cards required" required/>
</div>
</div>
<div class="col-md-2">
<label>GPU Memory</label>
<div class="form-group">
<input type="number" class="form-control" step="512" min="512"
placeholder="MB" required/>
</div>
</div>
</div>
<div>
<button id="form-job-submit" type="submit" class="btn btn-primary btn-lg">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- agent modal -->
<div class="modal fade" id="modal-agent" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content panel-info">
<div class="modal-header panel-heading">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 id="modal-agent-title" class="modal-title">Add New Agent</h4>
</div>
<div class="modal-body">
<form class="form" action="javascript:void(0)">
<label>IP</label>
<div class="form-group form-group-lg">
<label for="form-agent-ip" class="sr-only">IP</label>
<input type="text" id="form-agent-ip" class="form-control" maxlength="64"
placeholder="10.0.0.1" required/>
</div>
<label>Alias</label>
<div class="form-group form-group-lg">
<label for="form-agent-alias" class="sr-only">Alias</label>
<input type="text" id="form-agent-alias" class="form-control" maxlength="32"
placeholder="bj.node1"/>
</div>
<label>Cluster</label>
<div class="form-group form-group-lg">
<label for="form-agent-cluster" class="sr-only">Cluster</label>
<select id="form-agent-cluster" class="form-control">
<option value="0">default</option>
</select>
</div>
<label>Token</label>
<div class="form-group form-group-lg">
<label for="form-agent-token" class="sr-only">Token</label>
<input type="text" id="form-agent-token" class="form-control" placeholder="******" readonly/>
</div>
<div>
<input type="hidden" id="form-agent-submit-type"/>
<input type="hidden" id="form-agent-id"/>
<button id="form-agent-submit" type="submit" class="btn btn-primary btn-lg">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>

14
predis/autoload.php Normal file
View File

@ -0,0 +1,14 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
require __DIR__.'/src/Autoloader.php';
Predis\Autoloader::register();

62
predis/src/Autoloader.php Normal file
View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
/**
* Implements a lightweight PSR-0 compliant autoloader for Predis.
*
* @author Eric Naeseth <eric@thumbtack.com>
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class Autoloader
{
private $directory;
private $prefix;
private $prefixLength;
/**
* @param string $baseDirectory Base directory where the source files are located.
*/
public function __construct($baseDirectory = __DIR__)
{
$this->directory = $baseDirectory;
$this->prefix = __NAMESPACE__.'\\';
$this->prefixLength = strlen($this->prefix);
}
/**
* Registers the autoloader class with the PHP SPL autoloader.
*
* @param bool $prepend Prepend the autoloader on the stack instead of appending it.
*/
public static function register($prepend = false)
{
spl_autoload_register(array(new self(), 'autoload'), true, $prepend);
}
/**
* Loads a class from a file using its fully qualified name.
*
* @param string $className Fully qualified name of a class.
*/
public function autoload($className)
{
if (0 === strpos($className, $this->prefix)) {
$parts = explode('\\', substr($className, $this->prefixLength));
$filepath = $this->directory.DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts).'.php';
if (is_file($filepath)) {
require $filepath;
}
}
}
}

547
predis/src/Client.php Normal file
View File

@ -0,0 +1,547 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
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 <suppakilla@gmail.com>
*/
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);
}
}

View File

@ -0,0 +1,198 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
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 <suppakilla@gmail.com>
*/
interface ClientContextInterface
{
/**
* Sends the specified command instance to Redis.
*
* @param CommandInterface $command Command instance.
*
* @return mixed
*/
public function executeCommand(CommandInterface $command);
/**
* Sends the specified command with its arguments to Redis.
*
* @param string $method Command ID.
* @param array $arguments Arguments for the command.
*
* @return mixed
*/
public function __call($method, $arguments);
/**
* Starts the execution of the context.
*
* @param mixed $callable Optional callback for execution.
*
* @return array
*/
public function execute($callable = null);
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
/**
* Exception class that identifies client-side errors.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ClientException extends PredisException
{
}

View File

@ -0,0 +1,239 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
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 <suppakilla@gmail.com>
*/
interface ClientInterface
{
/**
* Returns the server profile used by the client.
*
* @return ProfileInterface
*/
public function getProfile();
/**
* Returns the client options specified upon initialization.
*
* @return OptionsInterface
*/
public function getOptions();
/**
* Opens the underlying connection to the server.
*/
public function connect();
/**
* Closes the underlying connection from the server.
*/
public function disconnect();
/**
* Returns the underlying connection instance.
*
* @return ConnectionInterface
*/
public function getConnection();
/**
* Creates a new instance of the specified Redis command.
*
* @param string $method Command ID.
* @param array $arguments Arguments for the command.
*
* @return CommandInterface
*/
public function createCommand($method, $arguments = array());
/**
* Executes the specified Redis command.
*
* @param CommandInterface $command Command instance.
*
* @return mixed
*/
public function executeCommand(CommandInterface $command);
/**
* Creates a Redis command with the specified arguments and sends a request
* to the server.
*
* @param string $method Command ID.
* @param array $arguments Arguments for the command.
*
* @return mixed
*/
public function __call($method, $arguments);
}

View File

@ -0,0 +1,469 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use Predis\Command\CommandInterface;
use Predis\Command\ScriptCommand;
/**
* Common class implementing the logic needed to support clustering strategies.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
abstract class ClusterStrategy implements StrategyInterface
{
protected $commands;
/**
*
*/
public function __construct()
{
$this->commands = $this->getDefaultCommands();
}
/**
* Returns the default map of supported commands with their handlers.
*
* @return array
*/
protected function getDefaultCommands()
{
$getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument');
$getKeyFromAllArguments = array($this, 'getKeyFromAllArguments');
return array(
/* commands operating on the key space */
'EXISTS' => $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;
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*/
interface DistributorInterface
{
/**
* Adds a node to the distributor with an optional weight.
*
* @param mixed $node Node object.
* @param int $weight Weight for the node.
*/
public function add($node, $weight = null);
/**
* Removes a node from the distributor.
*
* @param mixed $node Node object.
*/
public function remove($node);
/**
* Returns the corresponding slot of a node from the distributor using the
* computed hash of a key.
*
* @param mixed $hash
*
* @return mixed
*/
public function getSlot($hash);
/**
* Returns a node from the distributor using its assigned slot ID.
*
* @param mixed $slot
*
* @return mixed|null
*/
public function getBySlot($slot);
/**
* Returns a node from the distributor using the computed hash of a key.
*
* @param mixed $hash
*
* @return mixed
*/
public function getByHash($hash);
/**
* Returns a node from the distributor mapping to the specified value.
*
* @param string $value
*
* @return mixed
*/
public function get($value);
/**
* Returns the underlying hash generator instance.
*
* @return HashGeneratorInterface
*/
public function getHashGenerator();
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster\Distributor;
/**
* Exception class that identifies empty rings.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class EmptyRingException extends \Exception
{
}

View File

@ -0,0 +1,270 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
* @author Lorenzo Castelli <lcastelli@gmail.com>
*/
class HashRing implements DistributorInterface, HashGeneratorInterface
{
const DEFAULT_REPLICAS = 128;
const DEFAULT_WEIGHT = 100;
private $ring;
private $ringKeys;
private $ringKeysCount;
private $replicas;
private $nodeHashCallback;
private $nodes = array();
/**
* @param int $replicas Number of replicas in the ring.
* @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
*/
public function __construct($replicas = self::DEFAULT_REPLICAS, $nodeHashCallback = null)
{
$this->replicas = $replicas;
$this->nodeHashCallback = $nodeHashCallback;
}
/**
* Adds a node to the ring with an optional weight.
*
* @param mixed $node Node object.
* @param int $weight Weight for the node.
*/
public function add($node, $weight = null)
{
// In case of collisions in the hashes of the nodes, the node added
// last wins, thus the order in which nodes are added is significant.
$this->nodes[] = array(
'object' => $node,
'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT,
);
$this->reset();
}
/**
* {@inheritdoc}
*/
public function remove($node)
{
// A node is removed by resetting the ring so that it's recreated from
// scratch, in order to reassign possible hashes with collisions to the
// right node according to the order in which they were added in the
// first place.
for ($i = 0; $i < count($this->nodes); ++$i) {
if ($this->nodes[$i]['object'] === $node) {
array_splice($this->nodes, $i, 1);
$this->reset();
break;
}
}
}
/**
* Resets the distributor.
*/
private function reset()
{
unset(
$this->ring,
$this->ringKeys,
$this->ringKeysCount
);
}
/**
* Returns the initialization status of the distributor.
*
* @return bool
*/
private function isInitialized()
{
return isset($this->ringKeys);
}
/**
* Calculates the total weight of all the nodes in the distributor.
*
* @return int
*/
private function computeTotalWeight()
{
$totalWeight = 0;
foreach ($this->nodes as $node) {
$totalWeight += $node['weight'];
}
return $totalWeight;
}
/**
* Initializes the distributor.
*/
private function initialize()
{
if ($this->isInitialized()) {
return;
}
if (!$this->nodes) {
throw new EmptyRingException('Cannot initialize an empty hashring.');
}
$this->ring = array();
$totalWeight = $this->computeTotalWeight();
$nodesCount = count($this->nodes);
foreach ($this->nodes as $node) {
$weightRatio = $node['weight'] / $totalWeight;
$this->addNodeToRing($this->ring, $node, $nodesCount, $this->replicas, $weightRatio);
}
ksort($this->ring, SORT_NUMERIC);
$this->ringKeys = array_keys($this->ring);
$this->ringKeysCount = count($this->ringKeys);
}
/**
* Implements the logic needed to add a node to the hashring.
*
* @param array $ring Source hashring.
* @param mixed $node Node object to be added.
* @param int $totalNodes Total number of nodes.
* @param int $replicas Number of replicas in the ring.
* @param float $weightRatio Weight ratio for the node.
*/
protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
{
$nodeObject = $node['object'];
$nodeHash = $this->getNodeHash($nodeObject);
$replicas = (int) round($weightRatio * $totalNodes * $replicas);
for ($i = 0; $i < $replicas; ++$i) {
$key = crc32("$nodeHash:$i");
$ring[$key] = $nodeObject;
}
}
/**
* {@inheritdoc}
*/
protected function getNodeHash($nodeObject)
{
if (!isset($this->nodeHashCallback)) {
return (string) $nodeObject;
}
return call_user_func($this->nodeHashCallback, $nodeObject);
}
/**
* {@inheritdoc}
*/
public function hash($value)
{
return crc32($value);
}
/**
* {@inheritdoc}
*/
public function getByHash($hash)
{
return $this->ring[$this->getSlot($hash)];
}
/**
* {@inheritdoc}
*/
public function getBySlot($slot)
{
$this->initialize();
if (isset($this->ring[$slot])) {
return $this->ring[$slot];
}
}
/**
* {@inheritdoc}
*/
public function getSlot($hash)
{
$this->initialize();
$ringKeys = $this->ringKeys;
$upper = $this->ringKeysCount - 1;
$lower = 0;
while ($lower <= $upper) {
$index = ($lower + $upper) >> 1;
$item = $ringKeys[$index];
if ($item > $hash) {
$upper = $index - 1;
} elseif ($item < $hash) {
$lower = $index + 1;
} else {
return $item;
}
}
return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)];
}
/**
* {@inheritdoc}
*/
public function get($value)
{
$hash = $this->hash($value);
$node = $this->getByHash($hash);
return $node;
}
/**
* Implements a strategy to deal with wrap-around errors during binary searches.
*
* @param int $upper
* @param int $lower
* @param int $ringKeysCount
*
* @return int
*/
protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
{
// Binary search for the last item in ringkeys with a value less or
// equal to the key. If no such item exists, return the last item.
return $upper >= 0 ? $upper : $ringKeysCount - 1;
}
/**
* {@inheritdoc}
*/
public function getHashGenerator()
{
return $this;
}
}

View File

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
* @author Lorenzo Castelli <lcastelli@gmail.com>
*/
class KetamaRing extends HashRing
{
const DEFAULT_REPLICAS = 160;
/**
* @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
*/
public function __construct($nodeHashCallback = null)
{
parent::__construct($this::DEFAULT_REPLICAS, $nodeHashCallback);
}
/**
* {@inheritdoc}
*/
protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
{
$nodeObject = $node['object'];
$nodeHash = $this->getNodeHash($nodeObject);
$replicas = (int) floor($weightRatio * $totalNodes * ($replicas / 4));
for ($i = 0; $i < $replicas; ++$i) {
$unpackedDigest = unpack('V4', md5("$nodeHash-$i", true));
foreach ($unpackedDigest as $key) {
$ring[$key] = $nodeObject;
}
}
}
/**
* {@inheritdoc}
*/
public function hash($value)
{
$hash = unpack('V', md5($value, true));
return $hash[1];
}
/**
* {@inheritdoc}
*/
protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
{
// Binary search for the first item in ringkeys with a value greater
// or equal to the key. If no such item exists, return the first item.
return $lower < $ringKeysCount ? $lower : 0;
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster\Hash;
/**
* Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class CRC16 implements HashGeneratorInterface
{
private static $CCITT_16 = array(
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,
);
/**
* {@inheritdoc}
*/
public function hash($value)
{
// CRC-CCITT-16 algorithm
$crc = 0;
$CCITT_16 = self::$CCITT_16;
$strlen = strlen($value);
for ($i = 0; $i < $strlen; ++$i) {
$crc = (($crc << 8) ^ $CCITT_16[($crc >> 8) ^ ord($value[$i])]) & 0xFFFF;
}
return $crc;
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster\Hash;
/**
* An hash generator implements the logic used to calculate the hash of a key to
* distribute operations among Redis nodes.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
interface HashGeneratorInterface
{
/**
* Generates an hash from a string to be used for distribution.
*
* @param string $value String value.
*
* @return int
*/
public function hash($value);
}

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*/
class PredisStrategy extends ClusterStrategy
{
protected $distributor;
/**
* @param DistributorInterface $distributor Optional distributor instance.
*/
public function __construct(DistributorInterface $distributor = null)
{
parent::__construct();
$this->distributor = $distributor ?: new HashRing();
}
/**
* {@inheritdoc}
*/
public function getSlotByKey($key)
{
$key = $this->extractKeyTag($key);
$hash = $this->distributor->hash($key);
$slot = $this->distributor->getSlot($hash);
return $slot;
}
/**
* {@inheritdoc}
*/
protected function checkSameSlotForKeys(array $keys)
{
if (!$count = count($keys)) {
return false;
}
$currentKey = $this->extractKeyTag($keys[0]);
for ($i = 1; $i < $count; ++$i) {
$nextKey = $this->extractKeyTag($keys[$i]);
if ($currentKey !== $nextKey) {
return false;
}
$currentKey = $nextKey;
}
return true;
}
/**
* {@inheritdoc}
*/
public function getDistributor()
{
return $this->distributor;
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*/
class RedisStrategy extends ClusterStrategy
{
protected $hashGenerator;
/**
* @param HashGeneratorInterface $hashGenerator Hash generator instance.
*/
public function __construct(HashGeneratorInterface $hashGenerator = null)
{
parent::__construct();
$this->hashGenerator = $hashGenerator ?: new CRC16();
}
/**
* {@inheritdoc}
*/
public function getSlotByKey($key)
{
$key = $this->extractKeyTag($key);
$slot = $this->hashGenerator->hash($key) & 0x3FFF;
return $slot;
}
/**
* {@inheritdoc}
*/
public function getDistributor()
{
throw new NotSupportedException(
'This cluster strategy does not provide an external distributor'
);
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*/
interface StrategyInterface
{
/**
* Returns a slot for the given command used for clustering distribution or
* NULL when this is not possible.
*
* @param CommandInterface $command Command instance.
*
* @return int
*/
public function getSlot(CommandInterface $command);
/**
* Returns a slot for the given key used for clustering distribution or NULL
* when this is not possible.
*
* @param string $key Key string.
*
* @return int
*/
public function getSlotByKey($key);
/**
* Returns a distributor instance to be used by the cluster.
*
* @return DistributorInterface
*/
public function getDistributor();
}

View File

@ -0,0 +1,191 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*/
abstract class CursorBasedIterator implements \Iterator
{
protected $client;
protected $match;
protected $count;
protected $valid;
protected $fetchmore;
protected $elements;
protected $cursor;
protected $position;
protected $current;
/**
* @param ClientInterface $client Client connected to Redis.
* @param string $match Pattern to match during the server-side iteration.
* @param int $count Hint used by Redis to compute the number of results per iteration.
*/
public function __construct(ClientInterface $client, $match = null, $count = null)
{
$this->client = $client;
$this->match = $match;
$this->count = $count;
$this->reset();
}
/**
* Ensures that the client supports the specified Redis command required to
* fetch elements from the server to perform the iteration.
*
* @param ClientInterface $client Client connected to Redis.
* @param string $commandID Command ID.
*
* @throws NotSupportedException
*/
protected function requiredCommand(ClientInterface $client, $commandID)
{
if (!$client->getProfile()->supportsCommand($commandID)) {
throw new NotSupportedException("The current profile does not support '$commandID'.");
}
}
/**
* Resets the inner state of the iterator.
*/
protected function reset()
{
$this->valid = true;
$this->fetchmore = true;
$this->elements = array();
$this->cursor = 0;
$this->position = -1;
$this->current = null;
}
/**
* Returns an array of options for the `SCAN` command.
*
* @return array
*/
protected function getScanOptions()
{
$options = array();
if (strlen($this->match) > 0) {
$options['MATCH'] = $this->match;
}
if ($this->count > 0) {
$options['COUNT'] = $this->count;
}
return $options;
}
/**
* Fetches a new set of elements from the remote collection, effectively
* advancing the iteration process.
*
* @return array
*/
abstract protected function executeCommand();
/**
* Populates the local buffer of elements fetched from the server during
* the iteration.
*/
protected function fetch()
{
list($cursor, $elements) = $this->executeCommand();
if (!$cursor) {
$this->fetchmore = false;
}
$this->cursor = $cursor;
$this->elements = $elements;
}
/**
* Extracts next values for key() and current().
*/
protected function extractNext()
{
++$this->position;
$this->current = array_shift($this->elements);
}
/**
* {@inheritdoc}
*/
public function rewind()
{
$this->reset();
$this->next();
}
/**
* {@inheritdoc}
*/
public function current()
{
return $this->current;
}
/**
* {@inheritdoc}
*/
public function key()
{
return $this->position;
}
/**
* {@inheritdoc}
*/
public function next()
{
tryFetch: {
if (!$this->elements && $this->fetchmore) {
$this->fetch();
}
if ($this->elements) {
$this->extractNext();
} elseif ($this->cursor) {
goto tryFetch;
} else {
$this->valid = false;
}
}
}
/**
* {@inheritdoc}
*/
public function valid()
{
return $this->valid;
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*
* @link http://redis.io/commands/scan
*/
class HashKey extends CursorBasedIterator
{
protected $key;
/**
* {@inheritdoc}
*/
public function __construct(ClientInterface $client, $key, $match = null, $count = null)
{
$this->requiredCommand($client, 'HSCAN');
parent::__construct($client, $match, $count);
$this->key = $key;
}
/**
* {@inheritdoc}
*/
protected function executeCommand()
{
return $this->client->hscan($this->key, $this->cursor, $this->getScanOptions());
}
/**
* {@inheritdoc}
*/
protected function extractNext()
{
if ($kv = each($this->elements)) {
$this->position = $kv[0];
$this->current = $kv[1];
unset($this->elements[$this->position]);
}
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*
* @link http://redis.io/commands/scan
*/
class Keyspace extends CursorBasedIterator
{
/**
* {@inheritdoc}
*/
public function __construct(ClientInterface $client, $match = null, $count = null)
{
$this->requiredCommand($client, 'SCAN');
parent::__construct($client, $match, $count);
}
/**
* {@inheritdoc}
*/
protected function executeCommand()
{
return $this->client->scan($this->cursor, $this->getScanOptions());
}
}

View File

@ -0,0 +1,176 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*
* @link http://redis.io/commands/lrange
*/
class ListKey implements \Iterator
{
protected $client;
protected $count;
protected $key;
protected $valid;
protected $fetchmore;
protected $elements;
protected $position;
protected $current;
/**
* @param ClientInterface $client Client connected to Redis.
* @param string $key Redis list key.
* @param int $count Number of items retrieved on each fetch operation.
*
* @throws \InvalidArgumentException
*/
public function __construct(ClientInterface $client, $key, $count = 10)
{
$this->requiredCommand($client, 'LRANGE');
if ((false === $count = filter_var($count, FILTER_VALIDATE_INT)) || $count < 0) {
throw new \InvalidArgumentException('The $count argument must be a positive integer.');
}
$this->client = $client;
$this->key = $key;
$this->count = $count;
$this->reset();
}
/**
* Ensures that the client instance supports the specified Redis command
* required to fetch elements from the server to perform the iteration.
*
* @param ClientInterface $client Client connected to Redis.
* @param string $commandID Command ID.
*
* @throws NotSupportedException
*/
protected function requiredCommand(ClientInterface $client, $commandID)
{
if (!$client->getProfile()->supportsCommand($commandID)) {
throw new NotSupportedException("The current profile does not support '$commandID'.");
}
}
/**
* Resets the inner state of the iterator.
*/
protected function reset()
{
$this->valid = true;
$this->fetchmore = true;
$this->elements = array();
$this->position = -1;
$this->current = null;
}
/**
* Fetches a new set of elements from the remote collection, effectively
* advancing the iteration process.
*
* @return array
*/
protected function executeCommand()
{
return $this->client->lrange($this->key, $this->position + 1, $this->position + $this->count);
}
/**
* Populates the local buffer of elements fetched from the server during the
* iteration.
*/
protected function fetch()
{
$elements = $this->executeCommand();
if (count($elements) < $this->count) {
$this->fetchmore = false;
}
$this->elements = $elements;
}
/**
* Extracts next values for key() and current().
*/
protected function extractNext()
{
++$this->position;
$this->current = array_shift($this->elements);
}
/**
* {@inheritdoc}
*/
public function rewind()
{
$this->reset();
$this->next();
}
/**
* {@inheritdoc}
*/
public function current()
{
return $this->current;
}
/**
* {@inheritdoc}
*/
public function key()
{
return $this->position;
}
/**
* {@inheritdoc}
*/
public function next()
{
if (!$this->elements && $this->fetchmore) {
$this->fetch();
}
if ($this->elements) {
$this->extractNext();
} else {
$this->valid = false;
}
}
/**
* {@inheritdoc}
*/
public function valid()
{
return $this->valid;
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*
* @link http://redis.io/commands/scan
*/
class SetKey extends CursorBasedIterator
{
protected $key;
/**
* {@inheritdoc}
*/
public function __construct(ClientInterface $client, $key, $match = null, $count = null)
{
$this->requiredCommand($client, 'SSCAN');
parent::__construct($client, $match, $count);
$this->key = $key;
}
/**
* {@inheritdoc}
*/
protected function executeCommand()
{
return $this->client->sscan($this->key, $this->cursor, $this->getScanOptions());
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\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 <suppakilla@gmail.com>
*
* @link http://redis.io/commands/scan
*/
class SortedSetKey extends CursorBasedIterator
{
protected $key;
/**
* {@inheritdoc}
*/
public function __construct(ClientInterface $client, $key, $match = null, $count = null)
{
$this->requiredCommand($client, 'ZSCAN');
parent::__construct($client, $match, $count);
$this->key = $key;
}
/**
* {@inheritdoc}
*/
protected function executeCommand()
{
return $this->client->zscan($this->key, $this->cursor, $this->getScanOptions());
}
/**
* {@inheritdoc}
*/
protected function extractNext()
{
if ($kv = each($this->elements)) {
$this->position = $kv[0];
$this->current = $kv[1];
unset($this->elements[$this->position]);
}
}
}

View File

@ -0,0 +1,129 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* Base class for Redis commands.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
abstract class Command implements CommandInterface
{
private $slot;
private $arguments = array();
/**
* Returns a filtered array of the arguments.
*
* @param array $arguments List of arguments.
*
* @return array
*/
protected function filterArguments(array $arguments)
{
return $arguments;
}
/**
* {@inheritdoc}
*/
public function setArguments(array $arguments)
{
$this->arguments = $this->filterArguments($arguments);
unset($this->slot);
}
/**
* {@inheritdoc}
*/
public function setRawArguments(array $arguments)
{
$this->arguments = $arguments;
unset($this->slot);
}
/**
* {@inheritdoc}
*/
public function getArguments()
{
return $this->arguments;
}
/**
* {@inheritdoc}
*/
public function getArgument($index)
{
if (isset($this->arguments[$index])) {
return $this->arguments[$index];
}
}
/**
* {@inheritdoc}
*/
public function setSlot($slot)
{
$this->slot = $slot;
}
/**
* {@inheritdoc}
*/
public function getSlot()
{
if (isset($this->slot)) {
return $this->slot;
}
}
/**
* {@inheritdoc}
*/
public function parseResponse($data)
{
return $data;
}
/**
* Normalizes the arguments array passed to a Redis command.
*
* @param array $arguments Arguments for a command.
*
* @return array
*/
public static function normalizeArguments(array $arguments)
{
if (count($arguments) === 1 && is_array($arguments[0])) {
return $arguments[0];
}
return $arguments;
}
/**
* Normalizes the arguments array passed to a variadic Redis command.
*
* @param array $arguments Arguments for a command.
*
* @return array
*/
public static function normalizeVariadic(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
return array_merge(array($arguments[0]), $arguments[1]);
}
return $arguments;
}
}

View File

@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* Defines an abstraction representing a Redis command.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
interface CommandInterface
{
/**
* Returns the ID of the Redis command. By convention, command identifiers
* must always be uppercase.
*
* @return string
*/
public function getId();
/**
* Assign the specified slot to the command for clustering distribution.
*
* @param int $slot Slot ID.
*/
public function setSlot($slot);
/**
* Returns the assigned slot of the command for clustering distribution.
*
* @return int|null
*/
public function getSlot();
/**
* Sets the arguments for the command.
*
* @param array $arguments List of arguments.
*/
public function setArguments(array $arguments);
/**
* Sets the raw arguments for the command without processing them.
*
* @param array $arguments List of arguments.
*/
public function setRawArguments(array $arguments);
/**
* Gets the arguments of the command.
*
* @return array
*/
public function getArguments();
/**
* Gets the argument of the command at the specified index.
*
* @param int $index Index of the desired argument.
*
* @return mixed|null
*/
public function getArgument($index);
/**
* Parses a raw response and returns a PHP object.
*
* @param string $data Binary string containing the whole response.
*
* @return mixed
*/
public function parseResponse($data);
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/auth
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ConnectionAuth extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'AUTH';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/echo
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ConnectionEcho extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'ECHO';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/ping
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ConnectionPing extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'PING';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/quit
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ConnectionQuit extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'QUIT';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/select
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ConnectionSelect extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'SELECT';
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/geoadd
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/geodist
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class GeospatialGeoDist extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'GEODIST';
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/geohash
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/geopos
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/georadius
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/georadiusbymember
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class GeospatialGeoRadiusByMember extends GeospatialGeoRadius
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'GEORADIUSBYMEMBER';
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hdel
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashDelete extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HDEL';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
return self::normalizeVariadic($arguments);
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hexists
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashExists extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HEXISTS';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hget
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashGet extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HGET';
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hgetall
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashGetAll extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HGETALL';
}
/**
* {@inheritdoc}
*/
public function parseResponse($data)
{
$result = array();
for ($i = 0; $i < count($data); ++$i) {
$result[$data[$i]] = $data[++$i];
}
return $result;
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hmget
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashGetMultiple extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HMGET';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
return self::normalizeVariadic($arguments);
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hincrby
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashIncrementBy extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HINCRBY';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hincrbyfloat
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashIncrementByFloat extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HINCRBYFLOAT';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hkeys
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashKeys extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HKEYS';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hlen
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashLength extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HLEN';
}
}

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hscan
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashScan extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HSCAN';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
if (count($arguments) === 3 && is_array($arguments[2])) {
$options = $this->prepareOptions(array_pop($arguments));
$arguments = array_merge($arguments, $options);
}
return $arguments;
}
/**
* Returns a list of options and modifiers compatible with Redis.
*
* @param array $options List of options.
*
* @return array
*/
protected function prepareOptions($options)
{
$options = array_change_key_case($options, CASE_UPPER);
$normalized = array();
if (!empty($options['MATCH'])) {
$normalized[] = 'MATCH';
$normalized[] = $options['MATCH'];
}
if (!empty($options['COUNT'])) {
$normalized[] = 'COUNT';
$normalized[] = $options['COUNT'];
}
return $normalized;
}
/**
* {@inheritdoc}
*/
public function parseResponse($data)
{
if (is_array($data)) {
$fields = $data[1];
$result = array();
for ($i = 0; $i < count($fields); ++$i) {
$result[$fields[$i]] = $fields[++$i];
}
$data[1] = $result;
}
return $data;
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hset
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashSet extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HSET';
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hmset
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashSetMultiple extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HMSET';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
$flattenedKVs = array($arguments[0]);
$args = $arguments[1];
foreach ($args as $k => $v) {
$flattenedKVs[] = $k;
$flattenedKVs[] = $v;
}
return $flattenedKVs;
}
return $arguments;
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hsetnx
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashSetPreserve extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HSETNX';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hstrlen
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashStringLength extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HSTRLEN';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/hvals
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HashValues extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'HVALS';
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/pfadd
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HyperLogLogAdd extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'PFADD';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
return self::normalizeVariadic($arguments);
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/pfcount
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HyperLogLogCount extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'PFCOUNT';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
return self::normalizeArguments($arguments);
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/pfmerge
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class HyperLogLogMerge extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'PFMERGE';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
return self::normalizeArguments($arguments);
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/del
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyDelete extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'DEL';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
return self::normalizeArguments($arguments);
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/dump
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyDump extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'DUMP';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/exists
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyExists extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'EXISTS';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/expire
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyExpire extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'EXPIRE';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/expireat
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyExpireAt extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'EXPIREAT';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/keys
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyKeys extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'KEYS';
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/migrate
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/move
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyMove extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'MOVE';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/persist
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyPersist extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'PERSIST';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/pexpire
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyPreciseExpire extends KeyExpire
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'PEXPIRE';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/pexpireat
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyPreciseExpireAt extends KeyExpireAt
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'PEXPIREAT';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/pttl
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyPreciseTimeToLive extends KeyTimeToLive
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'PTTL';
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/randomkey
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyRandom extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'RANDOMKEY';
}
/**
* {@inheritdoc}
*/
public function parseResponse($data)
{
return $data !== '' ? $data : null;
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/rename
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyRename extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'RENAME';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/renamenx
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyRenamePreserve extends KeyRename
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'RENAMENX';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/restore
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyRestore extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'RESTORE';
}
}

View File

@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/scan
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyScan extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'SCAN';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
$options = $this->prepareOptions(array_pop($arguments));
$arguments = array_merge($arguments, $options);
}
return $arguments;
}
/**
* Returns a list of options and modifiers compatible with Redis.
*
* @param array $options List of options.
*
* @return array
*/
protected function prepareOptions($options)
{
$options = array_change_key_case($options, CASE_UPPER);
$normalized = array();
if (!empty($options['MATCH'])) {
$normalized[] = 'MATCH';
$normalized[] = $options['MATCH'];
}
if (!empty($options['COUNT'])) {
$normalized[] = 'COUNT';
$normalized[] = $options['COUNT'];
}
return $normalized;
}
}

View File

@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/sort
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeySort extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'SORT';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
if (count($arguments) === 1) {
return $arguments;
}
$query = array($arguments[0]);
$sortParams = array_change_key_case($arguments[1], CASE_UPPER);
if (isset($sortParams['BY'])) {
$query[] = 'BY';
$query[] = $sortParams['BY'];
}
if (isset($sortParams['GET'])) {
$getargs = $sortParams['GET'];
if (is_array($getargs)) {
foreach ($getargs as $getarg) {
$query[] = 'GET';
$query[] = $getarg;
}
} else {
$query[] = 'GET';
$query[] = $getargs;
}
}
if (isset($sortParams['LIMIT']) &&
is_array($sortParams['LIMIT']) &&
count($sortParams['LIMIT']) == 2) {
$query[] = 'LIMIT';
$query[] = $sortParams['LIMIT'][0];
$query[] = $sortParams['LIMIT'][1];
}
if (isset($sortParams['SORT'])) {
$query[] = strtoupper($sortParams['SORT']);
}
if (isset($sortParams['ALPHA']) && $sortParams['ALPHA'] == true) {
$query[] = 'ALPHA';
}
if (isset($sortParams['STORE'])) {
$query[] = 'STORE';
$query[] = $sortParams['STORE'];
}
return $query;
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/ttl
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyTimeToLive extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'TTL';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/type
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class KeyType extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'TYPE';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/lindex
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ListIndex extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'LINDEX';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/linsert
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ListInsert extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'LINSERT';
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command;
/**
* @link http://redis.io/commands/llen
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ListLength extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'LLEN';
}
}

Some files were not shown because too many files have changed in this diff Show More