Client.php 29 KB

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