Client.php 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449
  1. <?php
  2. /**
  3. * Credis_Client (a fork of Redisent)
  4. *
  5. * Most commands are compatible with phpredis library:
  6. * - use "pipeline()" to start a pipeline of commands instead of multi(Redis::PIPELINE)
  7. * - any arrays passed as arguments will be flattened automatically
  8. * - setOption and getOption are not supported in standalone mode
  9. * - order of arguments follows redis-cli instead of phpredis where they differ (lrem)
  10. *
  11. * - Uses phpredis library if extension is installed for better performance.
  12. * - Establishes connection lazily.
  13. * - Supports tcp and unix sockets.
  14. * - Reconnects automatically unless a watch or transaction is in progress.
  15. * - Can set automatic retry connection attempts for iffy Redis connections.
  16. *
  17. * @author Colin Mollenhour <colin@mollenhour.com>
  18. * @copyright 2011 Colin Mollenhour <colin@mollenhour.com>
  19. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  20. * @package Credis_Client
  21. */
  22. if( ! defined('CRLF')) define('CRLF', sprintf('%s%s', chr(13), chr(10)));
  23. /**
  24. * Credis-specific errors, wraps native Redis errors
  25. */
  26. class CredisException extends Exception
  27. {
  28. const CODE_TIMED_OUT = 1;
  29. const CODE_DISCONNECTED = 2;
  30. public function __construct($message, $code = 0, $exception = NULL)
  31. {
  32. if ($exception && get_class($exception) == 'RedisException' && $message == 'read error on connection') {
  33. $code = CredisException::CODE_DISCONNECTED;
  34. }
  35. parent::__construct($message, $code, $exception);
  36. }
  37. }
  38. /**
  39. * Credis_Client, a lightweight Redis PHP standalone client and phpredis wrapper
  40. *
  41. * Server/Connection:
  42. * @method Credis_Client pipeline()
  43. * @method Credis_Client multi()
  44. * @method Credis_Client watch(string ...$keys)
  45. * @method Credis_Client unwatch()
  46. * @method array exec()
  47. * @method string|Credis_Client flushAll()
  48. * @method string|Credis_Client flushDb()
  49. * @method array|Credis_Client info(string $section = null)
  50. * @method bool|array|Credis_Client config(string $setGet, string $key, string $value = null)
  51. * @method array|Credis_Client role()
  52. * @method array|Credis_Client time()
  53. *
  54. * Keys:
  55. * @method int|Credis_Client del(string $key)
  56. * @method int|Credis_Client exists(string $key)
  57. * @method int|Credis_Client expire(string $key, int $seconds)
  58. * @method int|Credis_Client expireAt(string $key, int $timestamp)
  59. * @method array|Credis_Client keys(string $key)
  60. * @method int|Credis_Client persist(string $key)
  61. * @method bool|Credis_Client rename(string $key, string $newKey)
  62. * @method bool|Credis_Client renameNx(string $key, string $newKey)
  63. * @method array|Credis_Client sort(string $key, string $arg1, string $valueN = null)
  64. * @method int|Credis_Client ttl(string $key)
  65. * @method string|Credis_Client type(string $key)
  66. *
  67. * Scalars:
  68. * @method int|Credis_Client append(string $key, string $value)
  69. * @method int|Credis_Client decr(string $key)
  70. * @method int|Credis_Client decrBy(string $key, int $decrement)
  71. * @method bool|string|Credis_Client get(string $key)
  72. * @method int|Credis_Client getBit(string $key, int $offset)
  73. * @method string|Credis_Client getRange(string $key, int $start, int $end)
  74. * @method string|Credis_Client getSet(string $key, string $value)
  75. * @method int|Credis_Client incr(string $key)
  76. * @method int|Credis_Client incrBy(string $key, int $decrement)
  77. * @method array|Credis_Client mGet(array $keys)
  78. * @method bool|Credis_Client mSet(array $keysValues)
  79. * @method int|Credis_Client mSetNx(array $keysValues)
  80. * @method bool|Credis_Client set(string $key, string $value, int | array $options = null)
  81. * @method int|Credis_Client setBit(string $key, int $offset, int $value)
  82. * @method bool|Credis_Client setEx(string $key, int $seconds, string $value)
  83. * @method int|Credis_Client setNx(string $key, string $value)
  84. * @method int |Credis_Client setRange(string $key, int $offset, int $value)
  85. * @method int|Credis_Client strLen(string $key)
  86. *
  87. * Sets:
  88. * @method int|Credis_Client sAdd(string $key, mixed $value, string $valueN = null)
  89. * @method int|Credis_Client sRem(string $key, mixed $value, string $valueN = null)
  90. * @method array|Credis_Client sMembers(string $key)
  91. * @method array|Credis_Client sUnion(mixed $keyOrArray, string $valueN = null)
  92. * @method array|Credis_Client sInter(mixed $keyOrArray, string $valueN = null)
  93. * @method array |Credis_Client sDiff(mixed $keyOrArray, string $valueN = null)
  94. * @method string|Credis_Client sPop(string $key)
  95. * @method int|Credis_Client sCard(string $key)
  96. * @method int|Credis_Client sIsMember(string $key, string $member)
  97. * @method int|Credis_Client sMove(string $source, string $dest, string $member)
  98. * @method string|array|Credis_Client sRandMember(string $key, int $count = null)
  99. * @method int|Credis_Client sUnionStore(string $dest, string $key1, string $key2 = null)
  100. * @method int|Credis_Client sInterStore(string $dest, string $key1, string $key2 = null)
  101. * @method int|Credis_Client sDiffStore(string $dest, string $key1, string $key2 = null)
  102. *
  103. * Hashes:
  104. * @method bool|int|Credis_Client hSet(string $key, string $field, string $value)
  105. * @method bool|Credis_Client hSetNx(string $key, string $field, string $value)
  106. * @method bool|string|Credis_Client hGet(string $key, string $field)
  107. * @method bool|int|Credis_Client hLen(string $key)
  108. * @method bool|Credis_Client hDel(string $key, string $field)
  109. * @method array|Credis_Client hKeys(string $key, string $field)
  110. * @method array|Credis_Client hVals(string $key)
  111. * @method array|Credis_Client hGetAll(string $key)
  112. * @method bool|Credis_Client hExists(string $key, string $field)
  113. * @method int|Credis_Client hIncrBy(string $key, string $field, int $value)
  114. * @method bool|Credis_Client hMSet(string $key, array $keysValues)
  115. * @method array|Credis_Client hMGet(string $key, array $fields)
  116. *
  117. * Lists:
  118. * @method array|null|Credis_Client blPop(string $keyN, int $timeout)
  119. * @method array|null|Credis_Client brPop(string $keyN, int $timeout)
  120. * @method array|null |Credis_Client brPoplPush(string $source, string $destination, int $timeout)
  121. * @method string|null|Credis_Client lIndex(string $key, int $index)
  122. * @method int|Credis_Client lInsert(string $key, string $beforeAfter, string $pivot, string $value)
  123. * @method int|Credis_Client lLen(string $key)
  124. * @method string|null|Credis_Client lPop(string $key)
  125. * @method int|Credis_Client lPush(string $key, mixed $value, mixed $valueN = null)
  126. * @method int|Credis_Client lPushX(string $key, mixed $value)
  127. * @method array|Credis_Client lRange(string $key, int $start, int $stop)
  128. * @method int|Credis_Client lRem(string $key, int $count, mixed $value)
  129. * @method bool|Credis_Client lSet(string $key, int $index, mixed $value)
  130. * @method bool|Credis_Client lTrim(string $key, int $start, int $stop)
  131. * @method string|null|Credis_Client rPop(string $key)
  132. * @method string|null|Credis_Client rPoplPush(string $source, string $destination)
  133. * @method int|Credis_Client rPush(string $key, mixed $value, mixed $valueN = null)
  134. * @method int |Credis_Client rPushX(string $key, mixed $value)
  135. *
  136. * Sorted Sets:
  137. * @method int|Credis_Client zAdd(string $key, double $score, string $value)
  138. * @method int|Credis_Client zCard(string $key)
  139. * @method int|Credis_Client zSize(string $key)
  140. * @method int|Credis_Client zCount(string $key, mixed $start, mixed $stop)
  141. * @method int|Credis_Client zIncrBy(string $key, double $value, string $member)
  142. * @method array|Credis_Client zRangeByScore(string $key, mixed $start, mixed $stop, array $args = null)
  143. * @method array|Credis_Client zRevRangeByScore(string $key, mixed $start, mixed $stop, array $args = null)
  144. * @method int|Credis_Client zRemRangeByScore(string $key, mixed $start, mixed $stop)
  145. * @method array|Credis_Client zRange(string $key, mixed $start, mixed $stop, array $args = null)
  146. * @method array|Credis_Client zRevRange(string $key, mixed $start, mixed $stop, array $args = null)
  147. * @method int|Credis_Client zRank(string $key, string $member)
  148. * @method int|Credis_Client zRevRank(string $key, string $member)
  149. * @method int|Credis_Client zRem(string $key, string $member)
  150. * @method int|Credis_Client zDelete(string $key, string $member)
  151. * TODO
  152. *
  153. * Pub/Sub
  154. * @method int |Credis_Client publish(string $channel, string $message)
  155. * @method int|array|Credis_Client pubsub(string $subCommand, $arg = null)
  156. *
  157. * Scripting:
  158. * @method string|int|Credis_Client script(string $command, string $arg1 = null)
  159. * @method string|int|array|bool|Credis_Client eval(string $script, array $keys = null, array $args = null)
  160. * @method string|int|array|bool|Credis_Client evalSha(string $script, array $keys = null, array $args = null)
  161. */
  162. class Credis_Client {
  163. const TYPE_STRING = 'string';
  164. const TYPE_LIST = 'list';
  165. const TYPE_SET = 'set';
  166. const TYPE_ZSET = 'zset';
  167. const TYPE_HASH = 'hash';
  168. const TYPE_NONE = 'none';
  169. const FREAD_BLOCK_SIZE = 8192;
  170. /**
  171. * Socket connection to the Redis server or Redis library instance
  172. * @var resource|Redis
  173. */
  174. protected $redis;
  175. protected $redisMulti;
  176. /**
  177. * Host of the Redis server
  178. * @var string
  179. */
  180. protected $host;
  181. /**
  182. * Port on which the Redis server is running
  183. * @var integer
  184. */
  185. protected $port;
  186. /**
  187. * Timeout for connecting to Redis server
  188. * @var float
  189. */
  190. protected $timeout;
  191. /**
  192. * Timeout for reading response from Redis server
  193. * @var float
  194. */
  195. protected $readTimeout;
  196. /**
  197. * Unique identifier for persistent connections
  198. * @var string
  199. */
  200. protected $persistent;
  201. /**
  202. * @var bool
  203. */
  204. protected $closeOnDestruct = TRUE;
  205. /**
  206. * @var bool
  207. */
  208. protected $connected = FALSE;
  209. /**
  210. * @var bool
  211. */
  212. protected $standalone;
  213. /**
  214. * @var int
  215. */
  216. protected $maxConnectRetries = 0;
  217. /**
  218. * @var int
  219. */
  220. protected $connectFailures = 0;
  221. /**
  222. * @var bool
  223. */
  224. protected $usePipeline = FALSE;
  225. /**
  226. * @var array
  227. */
  228. protected $commandNames;
  229. /**
  230. * @var string
  231. */
  232. protected $commands;
  233. /**
  234. * @var bool
  235. */
  236. protected $isMulti = FALSE;
  237. /**
  238. * @var bool
  239. */
  240. protected $isWatching = FALSE;
  241. /**
  242. * @var string
  243. */
  244. protected $authPassword;
  245. /**
  246. * @var int
  247. */
  248. protected $selectedDb = 0;
  249. /**
  250. * Aliases for backwards compatibility with phpredis
  251. * @var array
  252. */
  253. protected $wrapperMethods = array('delete' => 'del', 'getkeys' => 'keys', 'sremove' => 'srem');
  254. /**
  255. * @var array
  256. */
  257. protected $renamedCommands;
  258. /**
  259. * @var int
  260. */
  261. protected $requests = 0;
  262. /**
  263. * @var bool
  264. */
  265. protected $subscribed = false;
  266. /**
  267. * Creates a Redisent connection to the Redis server on host {@link $host} and port {@link $port}.
  268. * $host may also be a path to a unix socket or a string in the form of tcp://[hostname]:[port] or unix://[path]
  269. *
  270. * @param string $host The hostname of the Redis server
  271. * @param integer $port The port number of the Redis server
  272. * @param float $timeout Timeout period in seconds
  273. * @param string $persistent Flag to establish persistent connection
  274. * @param int $db The selected datbase of the Redis server
  275. * @param string $password The authentication password of the Redis server
  276. */
  277. public function __construct($host = '127.0.0.1', $port = 6379, $timeout = null, $persistent = '', $db = 0, $password = null)
  278. {
  279. $this->host = (string) $host;
  280. $this->port = (int) $port;
  281. $this->timeout = $timeout;
  282. $this->persistent = (string) $persistent;
  283. $this->standalone = ! extension_loaded('redis');
  284. $this->authPassword = $password;
  285. $this->selectedDb = (int)$db;
  286. $this->convertHost();
  287. }
  288. public function __destruct()
  289. {
  290. if ($this->closeOnDestruct) {
  291. $this->close();
  292. }
  293. }
  294. /**
  295. * @return bool
  296. */
  297. public function isSubscribed()
  298. {
  299. return $this->subscribed;
  300. }
  301. /**
  302. * Return the host of the Redis instance
  303. * @return string
  304. */
  305. public function getHost()
  306. {
  307. return $this->host;
  308. }
  309. /**
  310. * Return the port of the Redis instance
  311. * @return int
  312. */
  313. public function getPort()
  314. {
  315. return $this->port;
  316. }
  317. /**
  318. * Return the selected database
  319. * @return int
  320. */
  321. public function getSelectedDb()
  322. {
  323. return $this->selectedDb;
  324. }
  325. /**
  326. * @return string
  327. */
  328. public function getPersistence()
  329. {
  330. return $this->persistent;
  331. }
  332. /**
  333. * @throws CredisException
  334. * @return Credis_Client
  335. */
  336. public function forceStandalone()
  337. {
  338. if ($this->standalone) {
  339. return $this;
  340. }
  341. if($this->connected) {
  342. throw new CredisException('Cannot force Credis_Client to use standalone PHP driver after a connection has already been established.');
  343. }
  344. $this->standalone = TRUE;
  345. return $this;
  346. }
  347. /**
  348. * @param int $retries
  349. * @return Credis_Client
  350. */
  351. public function setMaxConnectRetries($retries)
  352. {
  353. $this->maxConnectRetries = $retries;
  354. return $this;
  355. }
  356. /**
  357. * @param bool $flag
  358. * @return Credis_Client
  359. */
  360. public function setCloseOnDestruct($flag)
  361. {
  362. $this->closeOnDestruct = $flag;
  363. return $this;
  364. }
  365. protected function convertHost()
  366. {
  367. if (preg_match('#^(tcp|unix)://(.*)$#', $this->host, $matches)) {
  368. if($matches[1] == 'tcp') {
  369. if ( ! preg_match('#^([^:]+)(:([0-9]+))?(/(.+))?$#', $matches[2], $matches)) {
  370. throw new CredisException('Invalid host format; expected tcp://host[:port][/persistence_identifier]');
  371. }
  372. $this->host = $matches[1];
  373. $this->port = (int) (isset($matches[3]) ? $matches[3] : 6379);
  374. $this->persistent = isset($matches[5]) ? $matches[5] : '';
  375. } else {
  376. $this->host = $matches[2];
  377. $this->port = NULL;
  378. if (substr($this->host,0,1) != '/') {
  379. throw new CredisException('Invalid unix socket format; expected unix:///path/to/redis.sock');
  380. }
  381. }
  382. }
  383. if ($this->port !== NULL && substr($this->host,0,1) == '/') {
  384. $this->port = NULL;
  385. }
  386. }
  387. /**
  388. * @throws CredisException
  389. * @return Credis_Client
  390. */
  391. public function connect()
  392. {
  393. if ($this->connected) {
  394. return $this;
  395. }
  396. $this->close(true);
  397. if ($this->standalone) {
  398. $flags = STREAM_CLIENT_CONNECT;
  399. $remote_socket = $this->port === NULL
  400. ? 'unix://'.$this->host
  401. : 'tcp://'.$this->host.':'.$this->port;
  402. if ($this->persistent && $this->port !== NULL) {
  403. // Persistent connections to UNIX sockets are not supported
  404. $remote_socket .= '/'.$this->persistent;
  405. $flags = $flags | STREAM_CLIENT_PERSISTENT;
  406. }
  407. $result = $this->redis = @stream_socket_client($remote_socket, $errno, $errstr, $this->timeout !== null ? $this->timeout : 2.5, $flags);
  408. }
  409. else {
  410. if ( ! $this->redis) {
  411. $this->redis = new Redis;
  412. }
  413. $socketTimeout = $this->timeout ? $this->timeout : 0.0;
  414. try
  415. {
  416. $result = $this->persistent
  417. ? $this->redis->pconnect($this->host, $this->port, $socketTimeout, $this->persistent)
  418. : $this->redis->connect($this->host, $this->port, $socketTimeout);
  419. }
  420. catch(Exception $e)
  421. {
  422. // Some applications will capture the php error that phpredis can sometimes generate and throw it as an Exception
  423. $result = false;
  424. $errno = 1;
  425. $errstr = $e->getMessage();
  426. }
  427. }
  428. // Use recursion for connection retries
  429. if ( ! $result) {
  430. $this->connectFailures++;
  431. if ($this->connectFailures <= $this->maxConnectRetries) {
  432. return $this->connect();
  433. }
  434. $failures = $this->connectFailures;
  435. $this->connectFailures = 0;
  436. throw new CredisException("Connection to Redis {$this->host}:{$this->port} failed after $failures failures." . (isset($errno) && isset($errstr) ? "Last Error : ({$errno}) {$errstr}" : ""));
  437. }
  438. $this->connectFailures = 0;
  439. $this->connected = TRUE;
  440. // Set read timeout
  441. if ($this->readTimeout) {
  442. $this->setReadTimeout($this->readTimeout);
  443. }
  444. if($this->authPassword) {
  445. $this->auth($this->authPassword);
  446. }
  447. if($this->selectedDb !== 0) {
  448. $this->select($this->selectedDb);
  449. }
  450. return $this;
  451. }
  452. /**
  453. * @return bool
  454. */
  455. public function isConnected()
  456. {
  457. return $this->connected;
  458. }
  459. /**
  460. * Set the read timeout for the connection. Use 0 to disable timeouts entirely (or use a very long timeout
  461. * if not supported).
  462. *
  463. * @param int $timeout 0 (or -1) for no timeout, otherwise number of seconds
  464. * @throws CredisException
  465. * @return Credis_Client
  466. */
  467. public function setReadTimeout($timeout)
  468. {
  469. if ($timeout < -1) {
  470. throw new CredisException('Timeout values less than -1 are not accepted.');
  471. }
  472. $this->readTimeout = $timeout;
  473. if ($this->isConnected()) {
  474. if ($this->standalone) {
  475. $timeout = $timeout <= 0 ? 315360000 : $timeout; // Ten-year timeout
  476. stream_set_blocking($this->redis, TRUE);
  477. stream_set_timeout($this->redis, (int) floor($timeout), ($timeout - floor($timeout)) * 1000000);
  478. } else if (defined('Redis::OPT_READ_TIMEOUT')) {
  479. // supported in phpredis 2.2.3
  480. // a timeout value of -1 means reads will not timeout
  481. $timeout = $timeout == 0 ? -1 : $timeout;
  482. $this->redis->setOption(Redis::OPT_READ_TIMEOUT, $timeout);
  483. }
  484. }
  485. return $this;
  486. }
  487. /**
  488. * @return bool
  489. */
  490. public function close($force = FALSE)
  491. {
  492. $result = TRUE;
  493. if ($this->redis && ($force || $this->connected && ! $this->persistent)) {
  494. try {
  495. if (is_callable(array($this->redis, 'close'))) {
  496. $this->redis->close();
  497. } else {
  498. @fclose($this->redis);
  499. $this->redis = null;
  500. }
  501. } catch (Exception $e) {
  502. ; // Ignore exceptions on close
  503. }
  504. $this->connected = $this->usePipeline = $this->isMulti = $this->isWatching = FALSE;
  505. }
  506. return $result;
  507. }
  508. /**
  509. * Enabled command renaming and provide mapping method. Supported methods are:
  510. *
  511. * 1. renameCommand('foo') // Salted md5 hash for all commands -> md5('foo'.$command)
  512. * 2. renameCommand(function($command){ return 'my'.$command; }); // Callable
  513. * 3. renameCommand('get', 'foo') // Single command -> alias
  514. * 4. renameCommand(['get' => 'foo', 'set' => 'bar']) // Full map of [command -> alias]
  515. *
  516. * @param string|callable|array $command
  517. * @param string|null $alias
  518. * @return $this
  519. */
  520. public function renameCommand($command, $alias = NULL)
  521. {
  522. if ( ! $this->standalone) {
  523. $this->forceStandalone();
  524. }
  525. if ($alias === NULL) {
  526. $this->renamedCommands = $command;
  527. } else {
  528. if ( ! $this->renamedCommands) {
  529. $this->renamedCommands = array();
  530. }
  531. $this->renamedCommands[$command] = $alias;
  532. }
  533. return $this;
  534. }
  535. /**
  536. * @param $command
  537. * @return string
  538. */
  539. public function getRenamedCommand($command)
  540. {
  541. static $map;
  542. // Command renaming not enabled
  543. if ($this->renamedCommands === NULL) {
  544. return $command;
  545. }
  546. // Initialize command map
  547. if ($map === NULL) {
  548. if (is_array($this->renamedCommands)) {
  549. $map = $this->renamedCommands;
  550. } else {
  551. $map = array();
  552. }
  553. }
  554. // Generate and return cached result
  555. if ( ! isset($map[$command])) {
  556. // String means all commands are hashed with salted md5
  557. if (is_string($this->renamedCommands)) {
  558. $map[$command] = md5($this->renamedCommands.$command);
  559. }
  560. // Would already be set in $map if it was intended to be renamed
  561. else if (is_array($this->renamedCommands)) {
  562. return $command;
  563. }
  564. // User-supplied function
  565. else if (is_callable($this->renamedCommands)) {
  566. $map[$command] = call_user_func($this->renamedCommands, $command);
  567. }
  568. }
  569. return $map[$command];
  570. }
  571. /**
  572. * @param string $password
  573. * @return bool
  574. */
  575. public function auth($password)
  576. {
  577. $response = $this->__call('auth', array($password));
  578. $this->authPassword = $password;
  579. return $response;
  580. }
  581. /**
  582. * @param int $index
  583. * @return bool
  584. */
  585. public function select($index)
  586. {
  587. $response = $this->__call('select', array($index));
  588. $this->selectedDb = (int) $index;
  589. return $response;
  590. }
  591. /**
  592. * @param string|array $pattern
  593. * @return array
  594. */
  595. public function pUnsubscribe()
  596. {
  597. list($command, $channel, $subscribedChannels) = $this->__call('punsubscribe', func_get_args());
  598. $this->subscribed = $subscribedChannels > 0;
  599. return array($command, $channel, $subscribedChannels);
  600. }
  601. /**
  602. * @param int $Iterator
  603. * @param string $pattern
  604. * @param int $count
  605. * @return bool|array
  606. */
  607. public function scan(&$Iterator, $pattern = null, $count = null)
  608. {
  609. return $this->__call('scan', array(&$Iterator, $pattern, $count));
  610. }
  611. /**
  612. * @param int $Iterator
  613. * @param string $field
  614. * @param string $pattern
  615. * @param int $count
  616. * @return bool|array
  617. */
  618. public function hscan(&$Iterator, $field, $pattern = null, $count = null)
  619. {
  620. return $this->__call('hscan', array($field, &$Iterator, $pattern, $count));
  621. }
  622. /**
  623. * @param int $Iterator
  624. * @param string $field
  625. * @param string $pattern
  626. * @param int $Iterator
  627. * @return bool|array
  628. */
  629. public function sscan(&$Iterator, $field, $pattern = null, $count = null)
  630. {
  631. return $this->__call('sscan', array($field, &$Iterator, $pattern, $count));
  632. }
  633. /**
  634. * @param int $Iterator
  635. * @param string $field
  636. * @param string $pattern
  637. * @param int $Iterator
  638. * @return bool|array
  639. */
  640. public function zscan(&$Iterator, $field, $pattern = null, $count = null)
  641. {
  642. return $this->__call('zscan', array($field, &$Iterator, $pattern, $count));
  643. }
  644. /**
  645. * @param string|array $patterns
  646. * @param $callback
  647. * @return $this|array|bool|Credis_Client|mixed|null|string
  648. * @throws CredisException
  649. */
  650. public function pSubscribe($patterns, $callback)
  651. {
  652. if ( ! $this->standalone) {
  653. return $this->__call('pSubscribe', array((array)$patterns, $callback));
  654. }
  655. // Standalone mode: use infinite loop to subscribe until timeout
  656. $patternCount = is_array($patterns) ? count($patterns) : 1;
  657. while ($patternCount--) {
  658. if (isset($status)) {
  659. list($command, $pattern, $status) = $this->read_reply();
  660. } else {
  661. list($command, $pattern, $status) = $this->__call('psubscribe', array($patterns));
  662. }
  663. $this->subscribed = $status > 0;
  664. if ( ! $status) {
  665. throw new CredisException('Invalid pSubscribe response.');
  666. }
  667. }
  668. while ($this->subscribed) {
  669. list($type, $pattern, $channel, $message) = $this->read_reply();
  670. if ($type != 'pmessage') {
  671. throw new CredisException('Received non-pmessage reply.');
  672. }
  673. $callback($this, $pattern, $channel, $message);
  674. }
  675. return null;
  676. }
  677. /**
  678. * @param string|array $pattern
  679. * @return array
  680. */
  681. public function unsubscribe()
  682. {
  683. list($command, $channel, $subscribedChannels) = $this->__call('unsubscribe', func_get_args());
  684. $this->subscribed = $subscribedChannels > 0;
  685. return array($command, $channel, $subscribedChannels);
  686. }
  687. /**
  688. * @param string|array $channels
  689. * @param $callback
  690. * @throws CredisException
  691. * @return $this|array|bool|Credis_Client|mixed|null|string
  692. */
  693. public function subscribe($channels, $callback)
  694. {
  695. if ( ! $this->standalone) {
  696. return $this->__call('subscribe', array((array)$channels, $callback));
  697. }
  698. // Standalone mode: use infinite loop to subscribe until timeout
  699. $channelCount = is_array($channels) ? count($channels) : 1;
  700. while ($channelCount--) {
  701. if (isset($status)) {
  702. list($command, $channel, $status) = $this->read_reply();
  703. } else {
  704. list($command, $channel, $status) = $this->__call('subscribe', array($channels));
  705. }
  706. $this->subscribed = $status > 0;
  707. if ( ! $status) {
  708. throw new CredisException('Invalid subscribe response.');
  709. }
  710. }
  711. while ($this->subscribed) {
  712. list($type, $channel, $message) = $this->read_reply();
  713. if ($type != 'message') {
  714. throw new CredisException('Received non-message reply.');
  715. }
  716. $callback($this, $channel, $message);
  717. }
  718. return null;
  719. }
  720. public function __call($name, $args)
  721. {
  722. // Lazy connection
  723. $this->connect();
  724. $name = strtolower($name);
  725. // Send request via native PHP
  726. if($this->standalone)
  727. {
  728. $trackedArgs = array();
  729. switch ($name) {
  730. case 'eval':
  731. case 'evalsha':
  732. $script = array_shift($args);
  733. $keys = (array) array_shift($args);
  734. $eArgs = (array) array_shift($args);
  735. $args = array($script, count($keys), $keys, $eArgs);
  736. break;
  737. case 'zunionstore':
  738. $dest = array_shift($args);
  739. $keys = (array) array_shift($args);
  740. $weights = array_shift($args);
  741. $aggregate = array_shift($args);
  742. $args = array($dest, count($keys), $keys);
  743. if ($weights) {
  744. $args[] = (array) $weights;
  745. }
  746. if ($aggregate) {
  747. $args[] = $aggregate;
  748. }
  749. break;
  750. case 'set':
  751. // The php redis module has different behaviour with ttl
  752. // https://github.com/phpredis/phpredis#set
  753. if (count($args) === 3 && is_int($args[2])) {
  754. $args = array($args[0], $args[1], array('EX', $args[2]));
  755. } elseif (count($args) === 3 && is_array($args[2])) {
  756. $tmp_args = $args;
  757. $args = array($tmp_args[0], $tmp_args[1]);
  758. foreach ($tmp_args[2] as $k=>$v) {
  759. if (is_string($k)) {
  760. $args[] = array($k,$v);
  761. } elseif (is_int($k)) {
  762. $args[] = $v;
  763. }
  764. }
  765. unset($tmp_args);
  766. }
  767. break;
  768. case 'scan':
  769. $trackedArgs = array(&$args[0]);
  770. if (empty($trackedArgs[0]))
  771. {
  772. $trackedArgs[0] = 0;
  773. }
  774. $eArgs = array($trackedArgs[0]);
  775. if (!empty($args[1]))
  776. {
  777. $eArgs[] = 'MATCH';
  778. $eArgs[] = $args[1];
  779. }
  780. if (!empty($args[2]))
  781. {
  782. $eArgs[] = 'COUNT';
  783. $eArgs[] = $args[2];
  784. }
  785. $args = $eArgs;
  786. break;
  787. case 'sscan':
  788. case 'zscan':
  789. case 'hscan':
  790. $trackedArgs = array(&$args[1]);
  791. if (empty($trackedArgs[0]))
  792. {
  793. $trackedArgs[0] = 0;
  794. }
  795. $eArgs = array($args[0],$trackedArgs[0]);
  796. if (!empty($args[2]))
  797. {
  798. $eArgs[] = 'MATCH';
  799. $eArgs[] = $args[2];
  800. }
  801. if (!empty($args[3]))
  802. {
  803. $eArgs[] = 'COUNT';
  804. $eArgs[] = $args[3];
  805. }
  806. $args = $eArgs;
  807. break;
  808. case 'zrangebyscore':
  809. case 'zrevrangebyscore':
  810. case 'zrange':
  811. case 'zrevrange':
  812. if (isset($args[3]) && is_array($args[3])) {
  813. // map options
  814. $cArgs = array();
  815. if (!empty($args[3]['withscores'])) {
  816. $cArgs[] = 'withscores';
  817. }
  818. if (($name == 'zrangebyscore' || $name == 'zrevrangebyscore') && array_key_exists('limit', $args[3])) {
  819. $cArgs[] = array('limit' => $args[3]['limit']);
  820. }
  821. $args[3] = $cArgs;
  822. $trackedArgs = $cArgs;
  823. }
  824. break;
  825. case 'mget':
  826. if (isset($args[0]) && is_array($args[0]))
  827. {
  828. $args = array_values($args[0]);
  829. }
  830. break;
  831. case 'hmset':
  832. if (isset($args[1]) && is_array($args[1]))
  833. {
  834. $cArgs = array();
  835. foreach($args[1] as $id => $value)
  836. {
  837. $cArgs[] = $id;
  838. $cArgs[] = $value;
  839. }
  840. $args[1] = $cArgs;
  841. }
  842. break;
  843. case 'zsize':
  844. $name = 'zcard';
  845. break;
  846. case 'zdelete':
  847. $name = 'zrem';
  848. break;
  849. case 'hmget':
  850. // hmget needs to track the keys for rehydrating the results
  851. if (isset($args[1]))
  852. {
  853. $trackedArgs = $args[1];
  854. }
  855. break;
  856. }
  857. // Flatten arguments
  858. $args = self::_flattenArguments($args);
  859. // In pipeline mode
  860. if($this->usePipeline)
  861. {
  862. if($name === 'pipeline') {
  863. throw new CredisException('A pipeline is already in use and only one pipeline is supported.');
  864. }
  865. else if($name === 'exec') {
  866. if($this->isMulti) {
  867. $this->commandNames[] = array($name, $trackedArgs);
  868. $this->commands .= self::_prepare_command(array($this->getRenamedCommand($name)));
  869. }
  870. // Write request
  871. if($this->commands) {
  872. $this->write_command($this->commands);
  873. }
  874. $this->commands = NULL;
  875. // Read response
  876. $queuedResponses = array();
  877. $response = array();
  878. foreach($this->commandNames as $command) {
  879. list($name, $arguments) = $command;
  880. $result = $this->read_reply($name, true);
  881. if ($result !== null)
  882. {
  883. $result = $this->decode_reply($name, $result, $arguments);
  884. }
  885. else
  886. {
  887. $queuedResponses[] = $command;
  888. }
  889. $response[] = $result;
  890. }
  891. if($this->isMulti) {
  892. $response = array_pop($response);
  893. foreach($queuedResponses as $key => $command)
  894. {
  895. list($name, $arguments) = $command;
  896. $response[$key] = $this->decode_reply($name, $response[$key], $arguments);
  897. }
  898. }
  899. $this->commandNames = NULL;
  900. $this->usePipeline = $this->isMulti = FALSE;
  901. return $response;
  902. }
  903. else if ($name === 'discard')
  904. {
  905. $this->commands = NULL;
  906. $this->commandNames = NULL;
  907. $this->usePipeline = $this->isMulti = FALSE;
  908. }
  909. else {
  910. if($name === 'multi') {
  911. $this->isMulti = TRUE;
  912. }
  913. array_unshift($args, $this->getRenamedCommand($name));
  914. $this->commandNames[] = array($name, $trackedArgs);
  915. $this->commands .= self::_prepare_command($args);
  916. return $this;
  917. }
  918. }
  919. // Start pipeline mode
  920. if($name === 'pipeline')
  921. {
  922. $this->usePipeline = TRUE;
  923. $this->commandNames = array();
  924. $this->commands = '';
  925. return $this;
  926. }
  927. // If unwatching, allow reconnect with no error thrown
  928. if($name === 'unwatch') {
  929. $this->isWatching = FALSE;
  930. }
  931. // Non-pipeline mode
  932. array_unshift($args, $this->getRenamedCommand($name));
  933. $command = self::_prepare_command($args);
  934. $this->write_command($command);
  935. $response = $this->read_reply($name);
  936. $response = $this->decode_reply($name, $response, $trackedArgs);
  937. // Watch mode disables reconnect so error is thrown
  938. if($name == 'watch') {
  939. $this->isWatching = TRUE;
  940. }
  941. // Transaction mode
  942. else if($this->isMulti && ($name == 'exec' || $name == 'discard')) {
  943. $this->isMulti = FALSE;
  944. }
  945. // Started transaction
  946. else if($this->isMulti || $name == 'multi') {
  947. $this->isMulti = TRUE;
  948. $response = $this;
  949. }
  950. }
  951. // Send request via phpredis client
  952. else
  953. {
  954. // Tweak arguments
  955. switch($name) {
  956. case 'get': // optimize common cases
  957. case 'set':
  958. case 'hget':
  959. case 'hset':
  960. case 'setex':
  961. case 'mset':
  962. case 'msetnx':
  963. case 'hmset':
  964. case 'hmget':
  965. case 'del':
  966. case 'zrangebyscore':
  967. case 'zrevrangebyscore':
  968. break;
  969. case 'zrange':
  970. case 'zrevrange':
  971. if (isset($args[3]) && is_array($args[3]))
  972. {
  973. $cArgs = $args[3];
  974. $args[3] = !empty($cArgs['withscores']);
  975. }
  976. $args = self::_flattenArguments($args);
  977. break;
  978. case 'zunionstore':
  979. $cArgs = array();
  980. $cArgs[] = array_shift($args); // destination
  981. $cArgs[] = array_shift($args); // keys
  982. if(isset($args[0]) and isset($args[0]['weights'])) {
  983. $cArgs[] = (array) $args[0]['weights'];
  984. } else {
  985. $cArgs[] = null;
  986. }
  987. if(isset($args[0]) and isset($args[0]['aggregate'])) {
  988. $cArgs[] = strtoupper($args[0]['aggregate']);
  989. }
  990. $args = $cArgs;
  991. break;
  992. case 'mget':
  993. if(isset($args[0]) && ! is_array($args[0])) {
  994. $args = array($args);
  995. }
  996. break;
  997. case 'lrem':
  998. $args = array($args[0], $args[2], $args[1]);
  999. break;
  1000. case 'eval':
  1001. case 'evalsha':
  1002. if (isset($args[1]) && is_array($args[1])) {
  1003. $cKeys = $args[1];
  1004. } elseif (isset($args[1]) && is_string($args[1])) {
  1005. $cKeys = array($args[1]);
  1006. } else {
  1007. $cKeys = array();
  1008. }
  1009. if (isset($args[2]) && is_array($args[2])) {
  1010. $cArgs = $args[2];
  1011. } elseif (isset($args[2]) && is_string($args[2])) {
  1012. $cArgs = array($args[2]);
  1013. } else {
  1014. $cArgs = array();
  1015. }
  1016. $args = array($args[0], array_merge($cKeys, $cArgs), count($cKeys));
  1017. break;
  1018. case 'subscribe':
  1019. case 'psubscribe':
  1020. break;
  1021. case 'scan':
  1022. case 'sscan':
  1023. case 'hscan':
  1024. case 'zscan':
  1025. // allow phpredis to see the caller's reference
  1026. //$param_ref =& $args[0];
  1027. break;
  1028. default:
  1029. // Flatten arguments
  1030. $args = self::_flattenArguments($args);
  1031. }
  1032. try {
  1033. // Proxy pipeline mode to the phpredis library
  1034. if($name == 'pipeline' || $name == 'multi') {
  1035. if($this->isMulti) {
  1036. return $this;
  1037. } else {
  1038. $this->isMulti = TRUE;
  1039. $this->redisMulti = call_user_func_array(array($this->redis, $name), $args);
  1040. return $this;
  1041. }
  1042. }
  1043. else if($name == 'exec' || $name == 'discard') {
  1044. $this->isMulti = FALSE;
  1045. $response = $this->redisMulti->$name();
  1046. $this->redisMulti = NULL;
  1047. #echo "> $name : ".substr(print_r($response, TRUE),0,100)."\n";
  1048. return $response;
  1049. }
  1050. // Use aliases to be compatible with phpredis wrapper
  1051. if(isset($this->wrapperMethods[$name])) {
  1052. $name = $this->wrapperMethods[$name];
  1053. }
  1054. // Multi and pipeline return self for chaining
  1055. if($this->isMulti) {
  1056. call_user_func_array(array($this->redisMulti, $name), $args);
  1057. return $this;
  1058. }
  1059. // Send request, retry one time when using persistent connections on the first request only
  1060. $this->requests++;
  1061. try {
  1062. $response = call_user_func_array(array($this->redis, $name), $args);
  1063. } catch (RedisException $e) {
  1064. if ($this->persistent && $this->requests == 1 && $e->getMessage() == 'read error on connection') {
  1065. $this->close(true);
  1066. $this->connect();
  1067. $response = call_user_func_array(array($this->redis, $name), $args);
  1068. } else {
  1069. throw $e;
  1070. }
  1071. }
  1072. }
  1073. // Wrap exceptions
  1074. catch(RedisException $e) {
  1075. $code = 0;
  1076. if ( ! ($result = $this->redis->IsConnected())) {
  1077. $this->close(true);
  1078. $code = CredisException::CODE_DISCONNECTED;
  1079. }
  1080. throw new CredisException($e->getMessage(), $code, $e);
  1081. }
  1082. #echo "> $name : ".substr(print_r($response, TRUE),0,100)."\n";
  1083. // change return values where it is too difficult to minim in standalone mode
  1084. switch($name)
  1085. {
  1086. case 'type':
  1087. $typeMap = array(
  1088. self::TYPE_NONE,
  1089. self::TYPE_STRING,
  1090. self::TYPE_SET,
  1091. self::TYPE_LIST,
  1092. self::TYPE_ZSET,
  1093. self::TYPE_HASH,
  1094. );
  1095. $response = $typeMap[$response];
  1096. break;
  1097. // Handle scripting errors
  1098. case 'eval':
  1099. case 'evalsha':
  1100. case 'script':
  1101. $error = $this->redis->getLastError();
  1102. $this->redis->clearLastError();
  1103. if ($error && substr($error,0,8) == 'NOSCRIPT') {
  1104. $response = NULL;
  1105. } else if ($error) {
  1106. throw new CredisException($error);
  1107. }
  1108. break;
  1109. case 'exists':
  1110. // smooth over phpredis-v4 vs earlier difference to match documented credis return results
  1111. $response = (int) $response;
  1112. break;
  1113. default:
  1114. $error = $this->redis->getLastError();
  1115. $this->redis->clearLastError();
  1116. if ($error) {
  1117. throw new CredisException(rtrim($error));
  1118. }
  1119. break;
  1120. }
  1121. }
  1122. return $response;
  1123. }
  1124. protected function write_command($command)
  1125. {
  1126. // Reconnect on lost connection (Redis server "timeout" exceeded since last command)
  1127. if(feof($this->redis)) {
  1128. // If a watch or transaction was in progress and connection was lost, throw error rather than reconnect
  1129. // since transaction/watch state will be lost.
  1130. if(($this->isMulti && ! $this->usePipeline) || $this->isWatching) {
  1131. $this->close(true);
  1132. throw new CredisException('Lost connection to Redis server during watch or transaction.');
  1133. }
  1134. $this->close(true);
  1135. $this->connect();
  1136. if($this->authPassword) {
  1137. $this->auth($this->authPassword);
  1138. }
  1139. if($this->selectedDb != 0) {
  1140. $this->select($this->selectedDb);
  1141. }
  1142. }
  1143. $commandLen = strlen($command);
  1144. $lastFailed = FALSE;
  1145. for ($written = 0; $written < $commandLen; $written += $fwrite) {
  1146. $fwrite = fwrite($this->redis, substr($command, $written));
  1147. if ($fwrite === FALSE || ($fwrite == 0 && $lastFailed)) {
  1148. $this->close(true);
  1149. throw new CredisException('Failed to write entire command to stream');
  1150. }
  1151. $lastFailed = $fwrite == 0;
  1152. }
  1153. }
  1154. protected function read_reply($name = '', $returnQueued = false)
  1155. {
  1156. $reply = fgets($this->redis);
  1157. if($reply === FALSE) {
  1158. $info = stream_get_meta_data($this->redis);
  1159. $this->close(true);
  1160. if ($info['timed_out']) {
  1161. throw new CredisException('Read operation timed out.', CredisException::CODE_TIMED_OUT);
  1162. } else {
  1163. throw new CredisException('Lost connection to Redis server.', CredisException::CODE_DISCONNECTED);
  1164. }
  1165. }
  1166. $reply = rtrim($reply, CRLF);
  1167. #echo "> $name: $reply\n";
  1168. $replyType = substr($reply, 0, 1);
  1169. switch ($replyType) {
  1170. /* Error reply */
  1171. case '-':
  1172. if($this->isMulti || $this->usePipeline) {
  1173. $response = FALSE;
  1174. } else if ($name == 'evalsha' && substr($reply,0,9) == '-NOSCRIPT') {
  1175. $response = NULL;
  1176. } else {
  1177. throw new CredisException(substr($reply,0,4) == '-ERR' ? 'ERR '.substr($reply, 5) : substr($reply,1));
  1178. }
  1179. break;
  1180. /* Inline reply */
  1181. case '+':
  1182. $response = substr($reply, 1);
  1183. if($response == 'OK') {
  1184. return TRUE;
  1185. }
  1186. if($response == 'QUEUED') {
  1187. return $returnQueued ? null : true;
  1188. }
  1189. break;
  1190. /* Bulk reply */
  1191. case '$':
  1192. if ($reply == '$-1') return FALSE;
  1193. $size = (int) substr($reply, 1);
  1194. $response = stream_get_contents($this->redis, $size + 2);
  1195. if( ! $response) {
  1196. $this->close(true);
  1197. throw new CredisException('Error reading reply.');
  1198. }
  1199. $response = substr($response, 0, $size);
  1200. break;
  1201. /* Multi-bulk reply */
  1202. case '*':
  1203. $count = substr($reply, 1);
  1204. if ($count == '-1') return FALSE;
  1205. $response = array();
  1206. for ($i = 0; $i < $count; $i++) {
  1207. $response[] = $this->read_reply();
  1208. }
  1209. break;
  1210. /* Integer reply */
  1211. case ':':
  1212. $response = intval(substr($reply, 1));
  1213. break;
  1214. default:
  1215. throw new CredisException('Invalid response: '.print_r($reply, TRUE));
  1216. break;
  1217. }
  1218. return $response;
  1219. }
  1220. protected function decode_reply($name, $response, array &$arguments = array() )
  1221. {
  1222. // Smooth over differences between phpredis and standalone response
  1223. switch ($name)
  1224. {
  1225. case '': // Minor optimization for multi-bulk replies
  1226. break;
  1227. case 'config':
  1228. case 'hgetall':
  1229. $keys = $values = array();
  1230. while ($response)
  1231. {
  1232. $keys[] = array_shift($response);
  1233. $values[] = array_shift($response);
  1234. }
  1235. $response = count($keys) ? array_combine($keys, $values) : array();
  1236. break;
  1237. case 'info':
  1238. $lines = explode(CRLF, trim($response, CRLF));
  1239. $response = array();
  1240. foreach ($lines as $line)
  1241. {
  1242. if (!$line || substr($line, 0, 1) == '#')
  1243. {
  1244. continue;
  1245. }
  1246. list($key, $value) = explode(':', $line, 2);
  1247. $response[$key] = $value;
  1248. }
  1249. break;
  1250. case 'ttl':
  1251. if ($response === -1)
  1252. {
  1253. $response = false;
  1254. }
  1255. break;
  1256. case 'hmget':
  1257. if (count($arguments) != count($response))
  1258. {
  1259. throw new CredisException(
  1260. 'hmget arguments and response do not match: ' . print_r($arguments, true) . ' ' . print_r(
  1261. $response, true
  1262. )
  1263. );
  1264. }
  1265. // rehydrate results into key => value form
  1266. $response = array_combine($arguments, $response);
  1267. break;
  1268. case 'scan':
  1269. case 'sscan':
  1270. $arguments[0] = intval(array_shift($response));
  1271. $response = empty($response[0]) ? array() : $response[0];
  1272. break;
  1273. case 'hscan':
  1274. case 'zscan':
  1275. $arguments[0] = intval(array_shift($response));
  1276. $response = empty($response[0]) ? array() : $response[0];
  1277. if (!empty($response) && is_array($response))
  1278. {
  1279. $count = count($response);
  1280. $out = array();
  1281. for ($i = 0; $i < $count; $i += 2)
  1282. {
  1283. $out[$response[$i]] = $response[$i + 1];
  1284. }
  1285. $response = $out;
  1286. }
  1287. break;
  1288. case 'zrangebyscore':
  1289. case 'zrevrangebyscore':
  1290. case 'zrange':
  1291. case 'zrevrange':
  1292. if (in_array('withscores', $arguments, true))
  1293. {
  1294. // Map array of values into key=>score list like phpRedis does
  1295. $item = null;
  1296. $out = array();
  1297. foreach ($response as $value)
  1298. {
  1299. if ($item == null)
  1300. {
  1301. $item = $value;
  1302. }
  1303. else
  1304. {
  1305. // 2nd value is the score
  1306. $out[$item] = (float)$value;
  1307. $item = null;
  1308. }
  1309. }
  1310. $response = $out;
  1311. }
  1312. break;
  1313. }
  1314. return $response;
  1315. }
  1316. /**
  1317. * Build the Redis unified protocol command
  1318. *
  1319. * @param array $args
  1320. * @return string
  1321. */
  1322. private static function _prepare_command($args)
  1323. {
  1324. return sprintf('*%d%s%s%s', count($args), CRLF, implode(array_map(array('self', '_map'), $args), CRLF), CRLF);
  1325. }
  1326. private static function _map($arg)
  1327. {
  1328. return sprintf('$%d%s%s', strlen($arg), CRLF, $arg);
  1329. }
  1330. /**
  1331. * Flatten arguments
  1332. *
  1333. * If an argument is an array, the key is inserted as argument followed by the array values
  1334. * array('zrangebyscore', '-inf', 123, array('limit' => array('0', '1')))
  1335. * becomes
  1336. * array('zrangebyscore', '-inf', 123, 'limit', '0', '1')
  1337. *
  1338. * @param array $in
  1339. * @return array
  1340. */
  1341. private static function _flattenArguments(array $arguments, &$out = array())
  1342. {
  1343. foreach ($arguments as $key => $arg) {
  1344. if (!is_int($key)) {
  1345. $out[] = $key;
  1346. }
  1347. if (is_array($arg)) {
  1348. self::_flattenArguments($arg, $out);
  1349. } else {
  1350. $out[] = $arg;
  1351. }
  1352. }
  1353. return $out;
  1354. }
  1355. }