Client.php 47 KB

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