Client.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  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 array exec()
  45. * @method string flushAll()
  46. * @method string flushDb()
  47. * @method array info()
  48. * @method bool|array config(string $setGet, string $key, string $value = null)
  49. *
  50. * Keys:
  51. * @method int del(string $key)
  52. * @method int exists(string $key)
  53. * @method int expire(string $key, int $seconds)
  54. * @method int expireAt(string $key, int $timestamp)
  55. * @method array keys(string $key)
  56. * @method int persist(string $key)
  57. * @method bool rename(string $key, string $newKey)
  58. * @method bool renameNx(string $key, string $newKey)
  59. * @method array sort(string $key, string $arg1, string $valueN = null)
  60. * @method int ttl(string $key)
  61. * @method string type(string $key)
  62. *
  63. * Scalars:
  64. * @method int append(string $key, string $value)
  65. * @method int decr(string $key)
  66. * @method int decrBy(string $key, int $decrement)
  67. * @method bool|string get(string $key)
  68. * @method int getBit(string $key, int $offset)
  69. * @method string getRange(string $key, int $start, int $end)
  70. * @method string getSet(string $key, string $value)
  71. * @method int incr(string $key)
  72. * @method int incrBy(string $key, int $decrement)
  73. * @method array mGet(array $keys)
  74. * @method bool mSet(array $keysValues)
  75. * @method int mSetNx(array $keysValues)
  76. * @method bool set(string $key, string $value)
  77. * @method int setBit(string $key, int $offset, int $value)
  78. * @method bool setEx(string $key, int $seconds, string $value)
  79. * @method int setNx(string $key, string $value)
  80. * @method int setRange(string $key, int $offset, int $value)
  81. * @method int strLen(string $key)
  82. *
  83. * Sets:
  84. * @method int sAdd(string $key, mixed $value, string $valueN = null)
  85. * @method int sRem(string $key, mixed $value, string $valueN = null)
  86. * @method array sMembers(string $key)
  87. * @method array sUnion(mixed $keyOrArray, string $valueN = null)
  88. * @method array sInter(mixed $keyOrArray, string $valueN = null)
  89. * @method array sDiff(mixed $keyOrArray, string $valueN = null)
  90. * @method string sPop(string $key)
  91. * @method int sCard(string $key)
  92. * @method int sIsMember(string $key, string $member)
  93. * @method int sMove(string $source, string $dest, string $member)
  94. * @method string|array sRandMember(string $key, int $count = null)
  95. * @method int sUnionStore(string $dest, string $key1, string $key2 = null)
  96. * @method int sInterStore(string $dest, string $key1, string $key2 = null)
  97. * @method int sDiffStore(string $dest, string $key1, string $key2 = null)
  98. *
  99. * Hashes:
  100. * @method bool|int hSet(string $key, string $field, string $value)
  101. * @method bool hSetNx(string $key, string $field, string $value)
  102. * @method bool|string hGet(string $key, string $field)
  103. * @method bool|int hLen(string $key)
  104. * @method bool hDel(string $key, string $field)
  105. * @method array hKeys(string $key, string $field)
  106. * @method array hVals(string $key, string $field)
  107. * @method array hGetAll(string $key)
  108. * @method bool hExists(string $key, string $field)
  109. * @method int hIncrBy(string $key, string $field, int $value)
  110. * @method bool hMSet(string $key, array $keysValues)
  111. * @method array hMGet(string $key, array $fields)
  112. *
  113. * Lists:
  114. * @method array|null blPop(string $keyN, int $timeout)
  115. * @method array|null brPop(string $keyN, int $timeout)
  116. * @method array|null brPoplPush(string $source, string $destination, int $timeout)
  117. * @method string|null lIndex(string $key, int $index)
  118. * @method int lInsert(string $key, string $beforeAfter, string $pivot, string $value)
  119. * @method int lLen(string $key)
  120. * @method string|null lPop(string $key)
  121. * @method int lPush(string $key, mixed $value, mixed $valueN = null)
  122. * @method int lPushX(string $key, mixed $value)
  123. * @method array lRange(string $key, int $start, int $stop)
  124. * @method int lRem(string $key, int $count, mixed $value)
  125. * @method bool lSet(string $key, int $index, mixed $value)
  126. * @method bool lTrim(string $key, int $start, int $stop)
  127. * @method string|null rPop(string $key)
  128. * @method string|null rPoplPush(string $source, string $destination)
  129. * @method int rPush(string $key, mixed $value, mixed $valueN = null)
  130. * @method int rPushX(string $key, mixed $value)
  131. *
  132. * Sorted Sets:
  133. * TODO
  134. *
  135. * Pub/Sub
  136. * @method array pUnsubscribe(mixed $pattern, string $patternN = NULL))
  137. * @method array unsubscribe(mixed $channel, string $channelN = NULL))
  138. * @method int publish(string $channel, string $message)
  139. * @method int|array pubsub(string $subCommand, $arg = NULL)
  140. *
  141. * Scripting:
  142. * @method string|int script(string $command, string $arg1 = null)
  143. * @method string|int|array|bool eval(string $script, array $keys = NULL, array $args = NULL)
  144. * @method string|int|array|bool evalSha(string $script, array $keys = NULL, array $args = NULL)
  145. */
  146. class Redis_Credis_Client
  147. {
  148. const TYPE_STRING = 'string';
  149. const TYPE_LIST = 'list';
  150. const TYPE_SET = 'set';
  151. const TYPE_ZSET = 'zset';
  152. const TYPE_HASH = 'hash';
  153. const TYPE_NONE = 'none';
  154. const FREAD_BLOCK_SIZE = 8192;
  155. /**
  156. * Socket connection to the Redis server or Redis library instance
  157. * @var resource|Redis
  158. */
  159. protected $redis;
  160. protected $redisMulti;
  161. /**
  162. * Host of the Redis server
  163. * @var string
  164. */
  165. protected $host;
  166. /**
  167. * Port on which the Redis server is running
  168. * @var integer
  169. */
  170. protected $port;
  171. /**
  172. * Timeout for connecting to Redis server
  173. * @var float
  174. */
  175. protected $timeout;
  176. /**
  177. * Timeout for reading response from Redis server
  178. * @var float
  179. */
  180. protected $readTimeout;
  181. /**
  182. * Unique identifier for persistent connections
  183. * @var string
  184. */
  185. protected $persistent;
  186. /**
  187. * @var bool
  188. */
  189. protected $closeOnDestruct = TRUE;
  190. /**
  191. * @var bool
  192. */
  193. protected $connected = FALSE;
  194. /**
  195. * @var bool
  196. */
  197. protected $standalone;
  198. /**
  199. * @var int
  200. */
  201. protected $maxConnectRetries = 0;
  202. /**
  203. * @var int
  204. */
  205. protected $connectFailures = 0;
  206. /**
  207. * @var bool
  208. */
  209. protected $usePipeline = FALSE;
  210. /**
  211. * @var array
  212. */
  213. protected $commandNames;
  214. /**
  215. * @var string
  216. */
  217. protected $commands;
  218. /**
  219. * @var bool
  220. */
  221. protected $isMulti = FALSE;
  222. /**
  223. * @var bool
  224. */
  225. protected $isWatching = FALSE;
  226. /**
  227. * @var string
  228. */
  229. protected $authPassword;
  230. /**
  231. * @var int
  232. */
  233. protected $selectedDb = 0;
  234. /**
  235. * Aliases for backwards compatibility with phpredis
  236. * @var array
  237. */
  238. protected $wrapperMethods = array('delete' => 'del', 'getkeys' => 'keys', 'sremove' => 'srem');
  239. /**
  240. * @var array
  241. */
  242. protected $renamedCommands;
  243. /**
  244. * Creates a Redisent connection to the Redis server on host {@link $host} and port {@link $port}.
  245. * $host may also be a path to a unix socket or a string in the form of tcp://[hostname]:[port] or unix://[path]
  246. *
  247. * @param string $host The hostname of the Redis server
  248. * @param integer $port The port number of the Redis server
  249. * @param float $timeout Timeout period in seconds
  250. * @param string $persistent Flag to establish persistent connection
  251. */
  252. public function __construct($host = '127.0.0.1', $port = 6379, $timeout = null, $persistent = '')
  253. {
  254. $this->host = (string)$host;
  255. $this->port = (int)$port;
  256. $this->timeout = $timeout;
  257. $this->persistent = (string)$persistent;
  258. $this->standalone = !extension_loaded('redis');
  259. }
  260. public function __destruct()
  261. {
  262. if ($this->closeOnDestruct) {
  263. $this->close();
  264. }
  265. }
  266. /**
  267. * @throws CredisException
  268. * @return Credis_Client
  269. */
  270. public function forceStandalone()
  271. {
  272. if ($this->connected) {
  273. throw new CredisException('Cannot force Credis_Client to use standalone PHP driver after a connection has already been established.');
  274. }
  275. $this->standalone = TRUE;
  276. return $this;
  277. }
  278. /**
  279. * @param int $retries
  280. * @return Credis_Client
  281. */
  282. public function setMaxConnectRetries($retries)
  283. {
  284. $this->maxConnectRetries = $retries;
  285. return $this;
  286. }
  287. /**
  288. * @param bool $flag
  289. * @return Credis_Client
  290. */
  291. public function setCloseOnDestruct($flag)
  292. {
  293. $this->closeOnDestruct = $flag;
  294. return $this;
  295. }
  296. /**
  297. * @throws CredisException
  298. * @return Credis_Client
  299. */
  300. public function connect()
  301. {
  302. if ($this->connected) {
  303. return $this;
  304. }
  305. if (preg_match('#^(tcp|unix)://(.*)$#', $this->host, $matches)) {
  306. if ($matches[1] == 'tcp') {
  307. if (!preg_match('#^(.*)(?::(\d+))?(?:/(.*))?$#', $matches[2], $matches)) {
  308. throw new CredisException('Invalid host format; expected tcp://host[:port][/persistent]');
  309. }
  310. $this->host = $matches[1];
  311. $this->port = (int)(isset($matches[2]) ? $matches[2] : 6379);
  312. $this->persistent = isset($matches[3]) ? $matches[3] : '';
  313. } else {
  314. $this->host = $matches[2];
  315. $this->port = NULL;
  316. if (substr($this->host, 0, 1) != '/') {
  317. throw new CredisException('Invalid unix socket format; expected unix:///path/to/redis.sock');
  318. }
  319. }
  320. }
  321. if ($this->port !== NULL && substr($this->host, 0, 1) == '/') {
  322. $this->port = NULL;
  323. }
  324. if ($this->standalone) {
  325. $flags = STREAM_CLIENT_CONNECT;
  326. $remote_socket = $this->port === NULL
  327. ? 'unix://' . $this->host
  328. : 'tcp://' . $this->host . ':' . $this->port;
  329. if ($this->persistent) {
  330. if ($this->port === NULL) { // Unix socket
  331. throw new CredisException('Persistent connections to UNIX sockets are not supported in standalone mode.');
  332. }
  333. $remote_socket .= '/' . $this->persistent;
  334. $flags = $flags | STREAM_CLIENT_PERSISTENT;
  335. }
  336. $result = $this->redis = @stream_socket_client($remote_socket, $errno, $errstr, $this->timeout !== null ? $this->timeout : 2.5, $flags);
  337. } else {
  338. if (!$this->redis) {
  339. $this->redis = new Redis;
  340. }
  341. $result = $this->persistent
  342. ? $this->redis->pconnect($this->host, $this->port, $this->timeout, $this->persistent)
  343. : $this->redis->connect($this->host, $this->port, $this->timeout);
  344. }
  345. // Use recursion for connection retries
  346. if (!$result) {
  347. $this->connectFailures++;
  348. if ($this->connectFailures <= $this->maxConnectRetries) {
  349. return $this->connect();
  350. }
  351. $failures = $this->connectFailures;
  352. $this->connectFailures = 0;
  353. throw new CredisException("Connection to Redis failed after $failures failures." . (isset($errno) && isset($errstr) ? "Last Error : ({$errno}) {$errstr}" : ""));
  354. }
  355. $this->connectFailures = 0;
  356. $this->connected = TRUE;
  357. // Set read timeout
  358. if ($this->readTimeout) {
  359. $this->setReadTimeout($this->readTimeout);
  360. }
  361. return $this;
  362. }
  363. /**
  364. * Set the read timeout for the connection. Use 0 to disable timeouts entirely (or use a very long timeout
  365. * if not supported).
  366. *
  367. * @param int $timeout 0 (or -1) for no timeout, otherwise number of seconds
  368. * @throws CredisException
  369. * @return Credis_Client
  370. */
  371. public function setReadTimeout($timeout)
  372. {
  373. if ($timeout < -1) {
  374. throw new CredisException('Timeout values less than -1 are not accepted.');
  375. }
  376. $this->readTimeout = $timeout;
  377. if ($this->connected) {
  378. if ($this->standalone) {
  379. $timeout = $timeout <= 0 ? 315360000 : $timeout; // Ten-year timeout
  380. stream_set_blocking($this->redis, TRUE);
  381. stream_set_timeout($this->redis, (int)floor($timeout), ($timeout - floor($timeout)) * 1000000);
  382. } else if (defined('Redis::OPT_READ_TIMEOUT')) {
  383. // supported in phpredis 2.2.3
  384. // a timeout value of -1 means reads will not timeout
  385. $timeout = $timeout == 0 ? -1 : $timeout;
  386. $this->redis->setOption(Redis::OPT_READ_TIMEOUT, $timeout);
  387. }
  388. }
  389. return $this;
  390. }
  391. /**
  392. * @return bool
  393. */
  394. public function close()
  395. {
  396. $result = TRUE;
  397. if ($this->connected && !$this->persistent) {
  398. try {
  399. $result = $this->standalone ? fclose($this->redis) : $this->redis->close();
  400. $this->connected = FALSE;
  401. } catch (Exception $e) {
  402. ; // Ignore exceptions on close
  403. }
  404. }
  405. return $result;
  406. }
  407. /**
  408. * Enabled command renaming and provide mapping method. Supported methods are:
  409. *
  410. * 1. renameCommand('foo') // Salted md5 hash for all commands -> md5('foo'.$command)
  411. * 2. renameCommand(function($command){ return 'my'.$command; }); // Callable
  412. * 3. renameCommand('get', 'foo') // Single command -> alias
  413. * 4. renameCommand(['get' => 'foo', 'set' => 'bar']) // Full map of [command -> alias]
  414. *
  415. * @param string|callable|array $command
  416. * @param string|null $alias
  417. * @return $this
  418. */
  419. public function renameCommand($command, $alias = NULL)
  420. {
  421. if (!$this->standalone) {
  422. $this->forceStandalone();
  423. }
  424. if ($alias === NULL) {
  425. $this->renamedCommands = $command;
  426. } else {
  427. if (!$this->renamedCommands) {
  428. $this->renamedCommands = array();
  429. }
  430. $this->renamedCommands[$command] = $alias;
  431. }
  432. return $this;
  433. }
  434. /**
  435. * @param $command
  436. */
  437. public function getRenamedCommand($command)
  438. {
  439. static $map;
  440. // Command renaming not enabled
  441. if ($this->renamedCommands === NULL) {
  442. return $command;
  443. }
  444. // Initialize command map
  445. if ($map === NULL) {
  446. if (is_array($this->renamedCommands)) {
  447. $map = $this->renamedCommands;
  448. } else {
  449. $map = array();
  450. }
  451. }
  452. // Generate and return cached result
  453. if (!isset($map[$command])) {
  454. // String means all commands are hashed with salted md5
  455. if (is_string($this->renamedCommands)) {
  456. $map[$command] = md5($this->renamedCommands . $command);
  457. } // Would already be set in $map if it was intended to be renamed
  458. else if (is_array($this->renamedCommands)) {
  459. return $command;
  460. } // User-supplied function
  461. else if (is_callable($this->renamedCommands)) {
  462. $map[$command] = call_user_func($this->renamedCommands, $command);
  463. }
  464. }
  465. return $map[$command];
  466. }
  467. /**
  468. * @param string $password
  469. * @return bool
  470. */
  471. public function auth($password)
  472. {
  473. $this->authPassword = $password;
  474. $response = $this->__call('auth', array($this->authPassword));
  475. return $response;
  476. }
  477. /**
  478. * @param int $index
  479. * @return bool
  480. */
  481. public function select($index)
  482. {
  483. $this->selectedDb = (int)$index;
  484. $response = $this->__call('select', array($this->selectedDb));
  485. return $response;
  486. }
  487. /**
  488. * @param string|array $patterns
  489. * @param $callback
  490. * @return $this|array|bool|Credis_Client|mixed|null|string
  491. * @throws CredisException
  492. */
  493. public function pSubscribe($patterns, $callback)
  494. {
  495. if (!$this->standalone) {
  496. return $this->__call('pSubscribe', array((array)$patterns, $callback));
  497. }
  498. // Standalone mode: use infinite loop to subscribe until timeout
  499. $patternCount = is_array($patterns) ? count($patterns) : 1;
  500. while ($patternCount--) {
  501. if (isset($status)) {
  502. list($command, $pattern, $status) = $this->read_reply();
  503. } else {
  504. list($command, $pattern, $status) = $this->__call('psubscribe', array($patterns));
  505. }
  506. if (!$status) {
  507. throw new CredisException('Invalid pSubscribe response.');
  508. }
  509. }
  510. try {
  511. while (1) {
  512. list($type, $pattern, $channel, $message) = $this->read_reply();
  513. if ($type != 'pmessage') {
  514. throw new CredisException('Received non-pmessage reply.');
  515. }
  516. $callback($this, $pattern, $channel, $message);
  517. }
  518. } catch (CredisException $e) {
  519. if ($e->getCode() == CredisException::CODE_TIMED_OUT) {
  520. try {
  521. list($command, $pattern, $status) = $this->pUnsubscribe($patterns);
  522. while ($status !== 0) {
  523. list($command, $pattern, $status) = $this->read_reply();
  524. }
  525. } catch (CredisException $e2) {
  526. throw $e2;
  527. }
  528. }
  529. throw $e;
  530. }
  531. }
  532. /**
  533. * @param string|array $channels
  534. * @param $callback
  535. * @throws CredisException
  536. * @return $this|array|bool|Credis_Client|mixed|null|string
  537. */
  538. public function subscribe($channels, $callback)
  539. {
  540. if (!$this->standalone) {
  541. return $this->__call('subscribe', array((array)$channels, $callback));
  542. }
  543. // Standalone mode: use infinite loop to subscribe until timeout
  544. $channelCount = is_array($channels) ? count($channels) : 1;
  545. while ($channelCount--) {
  546. if (isset($status)) {
  547. list($command, $channel, $status) = $this->read_reply();
  548. } else {
  549. list($command, $channel, $status) = $this->__call('subscribe', array($channels));
  550. }
  551. if (!$status) {
  552. throw new CredisException('Invalid subscribe response.');
  553. }
  554. }
  555. try {
  556. while (1) {
  557. list($type, $channel, $message) = $this->read_reply();
  558. if ($type != 'message') {
  559. throw new CredisException('Received non-message reply.');
  560. }
  561. $callback($this, $channel, $message);
  562. }
  563. } catch (CredisException $e) {
  564. if ($e->getCode() == CredisException::CODE_TIMED_OUT) {
  565. try {
  566. list($command, $channel, $status) = $this->unsubscribe($channels);
  567. while ($status !== 0) {
  568. list($command, $channel, $status) = $this->read_reply();
  569. }
  570. } catch (CredisException $e2) {
  571. throw $e2;
  572. }
  573. }
  574. throw $e;
  575. }
  576. }
  577. public function __call($name, $args)
  578. {
  579. // Lazy connection
  580. $this->connect();
  581. $name = strtolower($name);
  582. // Send request via native PHP
  583. if ($this->standalone) {
  584. switch ($name) {
  585. case 'eval':
  586. case 'evalsha':
  587. $script = array_shift($args);
  588. $keys = (array)array_shift($args);
  589. $eArgs = (array)array_shift($args);
  590. $args = array($script, count($keys), $keys, $eArgs);
  591. break;
  592. }
  593. // Flatten arguments
  594. $argsFlat = NULL;
  595. foreach ($args as $index => $arg) {
  596. if (is_array($arg)) {
  597. if ($argsFlat === NULL) {
  598. $argsFlat = array_slice($args, 0, $index);
  599. }
  600. if ($name == 'mset' || $name == 'msetnx' || $name == 'hmset') {
  601. foreach ($arg as $key => $value) {
  602. $argsFlat[] = $key;
  603. $argsFlat[] = $value;
  604. }
  605. } else {
  606. $argsFlat = array_merge($argsFlat, $arg);
  607. }
  608. } else if ($argsFlat !== NULL) {
  609. $argsFlat[] = $arg;
  610. }
  611. }
  612. if ($argsFlat !== NULL) {
  613. $args = $argsFlat;
  614. $argsFlat = NULL;
  615. }
  616. // In pipeline mode
  617. if ($this->usePipeline) {
  618. if ($name == 'pipeline') {
  619. throw new CredisException('A pipeline is already in use and only one pipeline is supported.');
  620. } else if ($name == 'exec') {
  621. if ($this->isMulti) {
  622. $this->commandNames[] = $name;
  623. $this->commands .= self::_prepare_command(array($this->getRenamedCommand($name)));
  624. }
  625. // Write request
  626. if ($this->commands) {
  627. $this->write_command($this->commands);
  628. }
  629. $this->commands = NULL;
  630. // Read response
  631. $response = array();
  632. foreach ($this->commandNames as $command) {
  633. $response[] = $this->read_reply($command);
  634. }
  635. $this->commandNames = NULL;
  636. if ($this->isMulti) {
  637. $response = array_pop($response);
  638. }
  639. $this->usePipeline = $this->isMulti = FALSE;
  640. return $response;
  641. } else {
  642. if ($name == 'multi') {
  643. $this->isMulti = TRUE;
  644. }
  645. array_unshift($args, $this->getRenamedCommand($name));
  646. $this->commandNames[] = $name;
  647. $this->commands .= self::_prepare_command($args);
  648. return $this;
  649. }
  650. }
  651. // Start pipeline mode
  652. if ($name == 'pipeline') {
  653. $this->usePipeline = TRUE;
  654. $this->commandNames = array();
  655. $this->commands = '';
  656. return $this;
  657. }
  658. // If unwatching, allow reconnect with no error thrown
  659. if ($name == 'unwatch') {
  660. $this->isWatching = FALSE;
  661. }
  662. // Non-pipeline mode
  663. array_unshift($args, $this->getRenamedCommand($name));
  664. $command = self::_prepare_command($args);
  665. $this->write_command($command);
  666. $response = $this->read_reply($name);
  667. // Watch mode disables reconnect so error is thrown
  668. if ($name == 'watch') {
  669. $this->isWatching = TRUE;
  670. } // Transaction mode
  671. else if ($this->isMulti && ($name == 'exec' || $name == 'discard')) {
  672. $this->isMulti = FALSE;
  673. } // Started transaction
  674. else if ($this->isMulti || $name == 'multi') {
  675. $this->isMulti = TRUE;
  676. $response = $this;
  677. }
  678. } // Send request via phpredis client
  679. else {
  680. // Tweak arguments
  681. switch ($name) {
  682. case 'get': // optimize common cases
  683. case 'set':
  684. case 'hget':
  685. case 'hset':
  686. case 'setex':
  687. case 'mset':
  688. case 'msetnx':
  689. case 'hmset':
  690. case 'hmget':
  691. case 'del':
  692. break;
  693. case 'mget':
  694. if (isset($args[0]) && !is_array($args[0])) {
  695. $args = array($args);
  696. }
  697. break;
  698. case 'lrem':
  699. $args = array($args[0], $args[2], $args[1]);
  700. break;
  701. case 'eval':
  702. case 'evalsha':
  703. if (isset($args[1]) && is_array($args[1])) {
  704. $cKeys = $args[1];
  705. } elseif (isset($args[1]) && is_string($args[1])) {
  706. $cKeys = array($args[1]);
  707. } else {
  708. $cKeys = array();
  709. }
  710. if (isset($args[2]) && is_array($args[2])) {
  711. $cArgs = $args[2];
  712. } elseif (isset($args[2]) && is_string($args[2])) {
  713. $cArgs = array($args[2]);
  714. } else {
  715. $cArgs = array();
  716. }
  717. $args = array($args[0], array_merge($cKeys, $cArgs), count($cKeys));
  718. break;
  719. case 'subscribe':
  720. case 'psubscribe':
  721. break;
  722. default:
  723. // Flatten arguments
  724. $argsFlat = NULL;
  725. foreach ($args as $index => $arg) {
  726. if (is_array($arg)) {
  727. if ($argsFlat === NULL) {
  728. $argsFlat = array_slice($args, 0, $index);
  729. }
  730. $argsFlat = array_merge($argsFlat, $arg);
  731. } else if ($argsFlat !== NULL) {
  732. $argsFlat[] = $arg;
  733. }
  734. }
  735. if ($argsFlat !== NULL) {
  736. $args = $argsFlat;
  737. $argsFlat = NULL;
  738. }
  739. }
  740. try {
  741. // Proxy pipeline mode to the phpredis library
  742. if ($name == 'pipeline' || $name == 'multi') {
  743. if ($this->isMulti) {
  744. return $this;
  745. } else {
  746. $this->isMulti = TRUE;
  747. $this->redisMulti = call_user_func_array(array($this->redis, $name), $args);
  748. }
  749. } else if ($name == 'exec' || $name == 'discard') {
  750. $this->isMulti = FALSE;
  751. $response = $this->redisMulti->$name();
  752. $this->redisMulti = NULL;
  753. #echo "> $name : ".substr(print_r($response, TRUE),0,100)."\n";
  754. return $response;
  755. }
  756. // Use aliases to be compatible with phpredis wrapper
  757. if (isset($this->wrapperMethods[$name])) {
  758. $name = $this->wrapperMethods[$name];
  759. }
  760. // Multi and pipeline return self for chaining
  761. if ($this->isMulti) {
  762. call_user_func_array(array($this->redisMulti, $name), $args);
  763. return $this;
  764. }
  765. $response = call_user_func_array(array($this->redis, $name), $args);
  766. } // Wrap exceptions
  767. catch (RedisException $e) {
  768. $code = 0;
  769. if (!($result = $this->redis->IsConnected())) {
  770. $this->connected = FALSE;
  771. $code = CredisException::CODE_DISCONNECTED;
  772. }
  773. throw new CredisException($e->getMessage(), $code, $e);
  774. }
  775. #echo "> $name : ".substr(print_r($response, TRUE),0,100)."\n";
  776. // change return values where it is too difficult to minim in standalone mode
  777. switch ($name) {
  778. case 'hmget':
  779. $response = array_values($response);
  780. break;
  781. case 'type':
  782. $typeMap = array(
  783. self::TYPE_NONE,
  784. self::TYPE_STRING,
  785. self::TYPE_SET,
  786. self::TYPE_LIST,
  787. self::TYPE_ZSET,
  788. self::TYPE_HASH,
  789. );
  790. $response = $typeMap[$response];
  791. break;
  792. // Handle scripting errors
  793. case 'eval':
  794. case 'evalsha':
  795. case 'script':
  796. $error = $this->redis->getLastError();
  797. $this->redis->clearLastError();
  798. if ($error && substr($error, 0, 8) == 'NOSCRIPT') {
  799. $response = NULL;
  800. } else if ($error) {
  801. throw new CredisException($error);
  802. }
  803. break;
  804. }
  805. }
  806. return $response;
  807. }
  808. protected function write_command($command)
  809. {
  810. // Reconnect on lost connection (Redis server "timeout" exceeded since last command)
  811. if (feof($this->redis)) {
  812. $this->close();
  813. // If a watch or transaction was in progress and connection was lost, throw error rather than reconnect
  814. // since transaction/watch state will be lost.
  815. if (($this->isMulti && !$this->usePipeline) || $this->isWatching) {
  816. $this->isMulti = $this->isWatching = FALSE;
  817. throw new CredisException('Lost connection to Redis server during watch or transaction.');
  818. }
  819. $this->connected = FALSE;
  820. $this->connect();
  821. if ($this->authPassword) {
  822. $this->auth($this->authPassword);
  823. }
  824. if ($this->selectedDb != 0) {
  825. $this->select($this->selectedDb);
  826. }
  827. }
  828. $commandLen = strlen($command);
  829. for ($written = 0; $written < $commandLen; $written += $fwrite) {
  830. $fwrite = fwrite($this->redis, substr($command, $written));
  831. if ($fwrite === FALSE || $fwrite == 0) {
  832. $this->connected = FALSE;
  833. throw new CredisException('Failed to write entire command to stream');
  834. }
  835. }
  836. }
  837. protected function read_reply($name = '')
  838. {
  839. $reply = fgets($this->redis);
  840. if ($reply === FALSE) {
  841. $info = stream_get_meta_data($this->redis);
  842. if ($info['timed_out']) {
  843. throw new CredisException('Read operation timed out.', CredisException::CODE_TIMED_OUT);
  844. } else {
  845. $this->connected = FALSE;
  846. throw new CredisException('Lost connection to Redis server.', CredisException::CODE_DISCONNECTED);
  847. }
  848. }
  849. $reply = rtrim($reply, CRLF);
  850. #echo "> $name: $reply\n";
  851. $replyType = substr($reply, 0, 1);
  852. switch ($replyType) {
  853. /* Error reply */
  854. case '-':
  855. if ($this->isMulti || $this->usePipeline) {
  856. $response = FALSE;
  857. } else if ($name == 'evalsha' && substr($reply, 0, 9) == '-NOSCRIPT') {
  858. $response = NULL;
  859. } else {
  860. throw new CredisException(substr($reply, 0, 4) == '-ERR' ? substr($reply, 5) : substr($reply, 1));
  861. }
  862. break;
  863. /* Inline reply */
  864. case '+':
  865. $response = substr($reply, 1);
  866. if ($response == 'OK' || $response == 'QUEUED') {
  867. return TRUE;
  868. }
  869. break;
  870. /* Bulk reply */
  871. case '$':
  872. if ($reply == '$-1') return FALSE;
  873. $size = (int)substr($reply, 1);
  874. $response = stream_get_contents($this->redis, $size + 2);
  875. if (!$response) {
  876. $this->connected = FALSE;
  877. throw new CredisException('Error reading reply.');
  878. }
  879. $response = substr($response, 0, $size);
  880. break;
  881. /* Multi-bulk reply */
  882. case '*':
  883. $count = substr($reply, 1);
  884. if ($count == '-1') return FALSE;
  885. $response = array();
  886. for ($i = 0; $i < $count; $i++) {
  887. $response[] = $this->read_reply();
  888. }
  889. break;
  890. /* Integer reply */
  891. case ':':
  892. $response = intval(substr($reply, 1));
  893. break;
  894. default:
  895. throw new CredisException('Invalid response: ' . print_r($reply, TRUE));
  896. break;
  897. }
  898. // Smooth over differences between phpredis and standalone response
  899. switch ($name) {
  900. case '': // Minor optimization for multi-bulk replies
  901. break;
  902. case 'config':
  903. case 'hgetall':
  904. $keys = $values = array();
  905. while ($response) {
  906. $keys[] = array_shift($response);
  907. $values[] = array_shift($response);
  908. }
  909. $response = count($keys) ? array_combine($keys, $values) : array();
  910. break;
  911. case 'info':
  912. $lines = explode(CRLF, trim($response, CRLF));
  913. $response = array();
  914. foreach ($lines as $line) {
  915. if (!$line || substr($line, 0, 1) == '#') {
  916. continue;
  917. }
  918. list($key, $value) = explode(':', $line, 2);
  919. $response[$key] = $value;
  920. }
  921. break;
  922. case 'ttl':
  923. if ($response === -1) {
  924. $response = FALSE;
  925. }
  926. break;
  927. }
  928. return $response;
  929. }
  930. /**
  931. * Build the Redis unified protocol command
  932. *
  933. * @param array $args
  934. * @return string
  935. */
  936. private static function _prepare_command($args)
  937. {
  938. return sprintf('*%d%s%s%s', count($args), CRLF, implode(array_map(array('self', '_map'), $args), CRLF), CRLF);
  939. }
  940. private static function _map($arg)
  941. {
  942. return sprintf('$%d%s%s', strlen($arg), CRLF, $arg);
  943. }
  944. }