PageRenderTime 62ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/lib.kyoto.php

https://github.com/moechofe/KyotoTycoon-client-protocol
PHP | 1521 lines | 891 code | 142 blank | 488 comment | 124 complexity | 06eec62852b7afe5bd320998e839c508 MD5 | raw file
  1. <?php
  2. /**
  3. * Implementation of the Kyoto Tycoon Protocols (RPC and REST).
  4. * Author: martin mauchauffée
  5. * Link: http://github.com/moechofe/phpkyototycoon
  6. * Date: January 2012
  7. * Requirement: PHP 5.3+
  8. */
  9. namespace qad\kyoto;
  10. use Iterator, ArrayAccess, DomainException, OutOfBoundsException, RuntimeException, LogicException;
  11. /**
  12. * Return an UI object ready to send command to a KyotoTycoon server.
  13. * Params:
  14. * string $uri = The URI of the KyotoTycoon server.
  15. * Return:
  16. * UI = The User Interface object.
  17. * Set the connection parameters:
  18. * ----
  19. * $kt = UI(); // Default parameters is: localhost:1978 Corresponding to the first database loaded by the server.
  20. * $kt = UI('http://kt.local:1979/user.kch');
  21. * ----
  22. * Set and get value of the records:
  23. * ----
  24. * // Using ArrayAccess
  25. * $kt['japan'] = 'tokyo';
  26. * var_dump( $kt['japan']);
  27. * // Using method
  28. * $kt->set('france','paris');
  29. * var_dump( $kt->get('france') );
  30. * // Using magic method
  31. * $kt->coruscant('coruscant');
  32. * var_dump( $kt->coruscant );
  33. * ----
  34. * Set and get the expiration time of a record.
  35. * ----
  36. * $kt->set('a','ananas',2);
  37. * var_dump( $kt->gxt('a') );
  38. * ----
  39. * Browsing keys
  40. * ----
  41. * // Keys begins with a prefix
  42. * foreach( $kt->prefix('prefix_') as $key )
  43. * var_dump( $key );
  44. * // Keys matchs a regular expression
  45. * foreach( $kt->search('.*_match_.*') as $key )
  46. * var_dump( $key );
  47. * ----
  48. * Browsing records
  49. * // Keys begins with a prefix
  50. * foreach( $kt->begin('prefix_') as $key => $value )
  51. * var_dump( $key, $value );
  52. * // Keys matchs a regular expression
  53. * foreach( $kt->regex('.*_match_.*') as $key => $value )
  54. * var_dump( $key, $value );
  55. * // All records
  56. * foreach( $kt->forward() as $key => $value )
  57. * var_dump( $key, $value );
  58. * // All records starting at a key
  59. * foreach( $kt->forward('first') as $key => $value )
  60. * var_dump( $key, $value );
  61. * // All records in reverse order
  62. * foreach( $kt->backward() as $key => $value )
  63. * var_dump( $key, $value );
  64. * // All records in reverse order starting at a key
  65. * foreach( $kt->backward('last') as $key => $value )
  66. * var_dump( $key, $value );
  67. * ----
  68. */
  69. function UI( $uri = 'http://localhost:1978' )
  70. {
  71. static $instances = array();
  72. if( ! isset($instances[$uri]) )
  73. {
  74. assert('is_array(parse_url($uri))');
  75. $instances[$uri] = new UI( $uri );
  76. }
  77. return $instances[$uri];
  78. }
  79. // {{{ ConnectionException, InconsistencyException, ProtocolException
  80. /**
  81. * Thrown when the connection to the KyotoTycoon cannot be established.
  82. */
  83. class ConnectionException extends RuntimeException
  84. {
  85. function __construct( $uri, $msg )
  86. {
  87. parent::__construct( "Could'nt connect to KyotoTycoon server {$uri}. {$msg}", 1 );
  88. }
  89. }
  90. /**
  91. * Thrown when an operation is asked about a record that didn't respect all the needs.
  92. * The processing is done but the result is not fulfill the application logic.
  93. */
  94. class InconsistencyException extends OutOfBoundsException
  95. {
  96. function __construct( $uri, $msg )
  97. {
  98. parent::__construct( "(Un)existing record was detected on server {$uri}. {$msg}", 2 );
  99. }
  100. }
  101. /**
  102. * Throw if the protocol isn't well implemented for an operation.
  103. */
  104. class ProtocolException extends DomainException
  105. {
  106. function __construct( $uri )
  107. {
  108. parent::__construct( "Bad protocol communication with the KyotoTycoon server {$uri}.", 3 );
  109. }
  110. }
  111. class ImplementationException extends LogicException
  112. {
  113. function __construct( $uri )
  114. {
  115. parent::__construct( "Unimplented procedure on the selected database storage type with the KyotoTycoon server {$uri}.", 4 );
  116. }
  117. }
  118. // }}}
  119. /**
  120. * Fluent and quick user interface (UI) for the KyotoTycoon API.
  121. *
  122. */
  123. final class UI implements Iterator, ArrayAccess
  124. {
  125. // {{{ ---properties
  126. // The API object used to send command.
  127. private $api = null;
  128. function api() { return $this->api; }
  129. // Indicate if OutOfBoundsException should be throw instead of returning null.
  130. private $outofbound = true;
  131. // Indicate if RuntimeException should be throw instead of returning false.
  132. private $runtime = true;
  133. // Used to store the prefixe before initiate the process of browsing the keys.
  134. private $prefix = null;
  135. // Used to store the regex before intitiate the process of browsing the keys.
  136. private $regex = null;
  137. private $just_key = false;
  138. // Indicate the maximum number of keys returned by match_prefix and match_regex operations.
  139. private $max = null;
  140. // Used to store the retreived number of records founds with match_prefix and match_regex operations.
  141. private $num = null;
  142. // Used to store all the keys returned by match_prefix and match_regex operations.
  143. private $keys = null;
  144. // Used to store temporally the key and the value of a retrieved records during any browse operations.
  145. private $record = null;
  146. // Indicate the direction of the browsing operation.
  147. private $backward = null;
  148. // Set to store the current used cursor (CUR).
  149. private $cursor = null;
  150. // Indiquate the first key of a browsing operation.
  151. private $startkey = null;
  152. // Maintain a list of all used Kyoto Tycoon cursor (CUR).
  153. static $cursors = array();
  154. // }}}
  155. // {{{ __construct(), __clone()
  156. function __construct( $uri = 'http://localhost:1978' )
  157. {
  158. assert('is_array(parse_url($uri))');
  159. $this->api = new API( $uri );
  160. }
  161. function __destruct()
  162. {
  163. if( ! is_null($this->cursor) )
  164. {
  165. assert('is_integer($this->cursor)');
  166. unset(self::$cursors[$this->cursor]);
  167. }
  168. }
  169. function __clone()
  170. {
  171. $this->prefix = null;
  172. $this->regex = null;
  173. $this->just_key = false;
  174. $this->max = null;
  175. $this->num = null;
  176. $this->cursor = null;
  177. $this->keys = null;
  178. $this->record = null;
  179. $this->backward = null;
  180. $this->startkey = null;
  181. }
  182. // }}}
  183. // {{{ __get(), __isset(), __unset(), __call()
  184. function __get( $property )
  185. {
  186. assert('is_string($property)');
  187. switch( $property )
  188. {
  189. case 'api':
  190. return $this->api;
  191. case 'clear':
  192. $this->api->clear;
  193. return $this;
  194. case 'outofbound_throw_exception':
  195. $this->outofbound = true;
  196. return $this;
  197. case 'outofbound_return_null':
  198. $this->outofbound = false;
  199. return $this;
  200. case 'runtime_throw_exception':
  201. $this->runtime = true;
  202. return $this;
  203. case 'runtime_return_false':
  204. $this->runtime = false;
  205. return $this;
  206. default:
  207. try { return $this->api->get($property,$xt); }
  208. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  209. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  210. }
  211. }
  212. function __isset( $key )
  213. {
  214. assert('is_string($key)');
  215. try { return is_string($this->api->get($key,$xt)); }
  216. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return false; }
  217. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  218. }
  219. function __unset( $key )
  220. {
  221. assert('is_string($key)');
  222. $this->del($key);
  223. }
  224. function __call( $method, $args )
  225. {
  226. assert('is_string($method)');
  227. assert('is_scalar($args[0])');
  228. return $this->set($method, (string)$args[0]);
  229. }
  230. // }}}
  231. // {{{ get(), gxt(), set(), inc(), cat(), add(), rep(), del(), cas()
  232. /**
  233. * Retrieve the value of a record.
  234. * Params:
  235. * string $key = The key of the record.
  236. * (out) integer $xt = The absolute expiration time.
  237. * (out) null $xt = There is no expiration time.
  238. * Return:
  239. * string = The value of the record.
  240. * null = If the record do not exists.
  241. * false = If an error ocurred.
  242. */
  243. function get( $key, &$xt = null )
  244. {
  245. assert('is_string($key)');
  246. try { return $this->api->get($key,$xt); }
  247. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  248. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  249. }
  250. /**
  251. * Retrieve the expiration time of a record.
  252. * Params:
  253. * string $key = The key of the record.
  254. * Return:
  255. * string = The value of the expiration time.
  256. * null = If the record do not exists.
  257. * false = If an error ocurred.
  258. */
  259. function gxt( $key )
  260. {
  261. assert('is_string($key)');
  262. $xt = null;
  263. try { $this->api->get($key,$xt); return $xt; }
  264. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  265. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  266. }
  267. /**
  268. * Set the value of a record.
  269. * Params:
  270. * string $key = The key of the record.
  271. * string $value = The value of the record.
  272. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  273. * null $xt = No expiration time is specified.
  274. * Return:
  275. * true = If success.
  276. * false = If an error ocurred.
  277. */
  278. function set( $key, $value, $xt = null )
  279. {
  280. assert('is_string($key)');
  281. assert('is_string($value)');
  282. assert('is_null($xt) or is_numeric($xt)');
  283. try { $this->api->set($key,$value,$xt); return true; }
  284. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  285. }
  286. function inc( $key, $num = 1, $xt = null )
  287. {
  288. assert('is_string($key)');
  289. assert('is_numeric($num)');
  290. assert('is_null($xt) or is_numeric($xt)');
  291. try
  292. {
  293. if( is_integer($num) or (string)(int)$num===$num )
  294. return $this->api->increment( $key, $num, $xt );
  295. else
  296. return $this->api->increment_double( $key, $num, $xt );
  297. }
  298. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  299. }
  300. /**
  301. * Append the value to a record.
  302. * Params:
  303. * string $key = The key of the record.
  304. * string $value = The value of the record.
  305. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  306. * null $xt = No expiration time is specified.
  307. * Return:
  308. * true = If success.
  309. * false = If an error ocurred.
  310. */
  311. function cat( $key, $value, $xt = null )
  312. {
  313. assert('is_string($key)');
  314. assert('is_string($value)');
  315. assert('is_null($xt) or is_numeric($xt)');
  316. try { $this->api->append($key,$value,$xt); return true; }
  317. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  318. }
  319. /**
  320. * Add a record if it not exits.
  321. * Params:
  322. * string $key = The key of the record.
  323. * string $value = The value of the record.
  324. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  325. * null $xt = No expiration time is specified.
  326. * Return:
  327. * true = If success.
  328. * false = If an error ocurred.
  329. * null = If the record already exists.
  330. */
  331. function add( $key, $value, $xt = null )
  332. {
  333. assert('is_string($key)');
  334. assert('is_string($value)');
  335. assert('is_null($xt) or is_numeric($xt)');
  336. try { return $this->api->add($key,$value,$xt); }
  337. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  338. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  339. }
  340. /**
  341. * Replace the value of a record.
  342. * Params:
  343. * string $key = The key of the record.
  344. * string $value = The value of the record.
  345. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  346. * null $xt = No expiration time is specified.
  347. * Return:
  348. * true = If success.
  349. * false = If an error ocurred.
  350. * null = If the record don't exists.
  351. */
  352. function rep( $key, $value, $xt = null )
  353. {
  354. assert('is_string($key)');
  355. assert('is_string($value)');
  356. assert('is_null($xt) or is_numeric($xt)');
  357. try { return $this->api->replace($key,$value,$xt); }
  358. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  359. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  360. }
  361. /**
  362. * Remove the value of a record.
  363. * Params:
  364. * string $key = The key of the record.
  365. * Return:
  366. * true = If succes.
  367. * false = If an error ocurred.
  368. * null = If the record don't exists.
  369. */
  370. function del( $key )
  371. {
  372. assert('is_string($key)');
  373. try { $this->api->remove($key); return true; }
  374. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  375. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  376. }
  377. /**
  378. * Perform compare-and-swap.
  379. * Params:
  380. * string $key = The key of the record.
  381. * string $oval = The old value.
  382. * null $oval = If it is omittted, no record is meant.
  383. * string $nval = The new value.
  384. * null $nval = If it is omittted, the record is removed.
  385. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  386. * null $xt = No expiration time is specified.
  387. * Return:
  388. * true = If success.
  389. * false = If an error ocurred.
  390. * null = If the old value assumption was failed.
  391. */
  392. function cas( $key, $oval, $nval, $xt = null )
  393. {
  394. assert('is_string($key)');
  395. assert('is_string($oval) or is_null($oval)');
  396. assert('is_string($nval) or is_null($nval)');
  397. assert('is_null($xt) or is_numeric($xt)');
  398. try { return $this->api->cas($key,$oval,$nval,$xt); }
  399. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  400. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  401. }
  402. // }}}
  403. // {{{ begin(), search(), forward(), backward(), prefix(), regex()
  404. function begin( $prefix, $max = 0, &$num = null )
  405. {
  406. assert('is_string($prefix)');
  407. assert('is_numeric($max) and $max>=0 and (int)$max==$max');
  408. $stm = clone $this;
  409. $stm->prefix = $prefix;
  410. $stm->backward = false;
  411. $stm->max = $max;
  412. $stm->num = &$num;
  413. return $stm;
  414. }
  415. function reverse_begin( $prefix, $max = 0, &$num = null )
  416. {
  417. assert('is_string($prefix)');
  418. assert('is_numeric($max) and $max>=0 and (int)$max==$max');
  419. $stm = clone $this;
  420. $stm->prefix = $prefix;
  421. $stm->backward = true;
  422. $stm->max = $max;
  423. $stm->num = &$num;
  424. return $stm;
  425. }
  426. function search( $regex, $max = 0, &$num = null )
  427. {
  428. assert('is_string($regex)');
  429. assert('is_numeric($max) and $max>=0 and (int)$max==$max');
  430. $stm = clone $this;
  431. $stm->regex = $regex;
  432. $stm->backward = false;
  433. $stm->max = $max;
  434. $stm->num = &$num;
  435. return $stm;
  436. }
  437. function reverse_search( $regex, $max = 0, &$num = null )
  438. {
  439. assert('is_string($regex)');
  440. assert('is_numeric($max) and $max>=0 and (int)$max==$max');
  441. $stm = clone $this;
  442. $stm->regex = $regex;
  443. $stm->backward = true;
  444. $stm->max = $max;
  445. $stm->num = &$num;
  446. return $stm;
  447. }
  448. function forward( $key = null, $only_keys = false )
  449. {
  450. assert('is_string($key) or is_null($key)');
  451. assert('is_bool($only_keys)');
  452. $stm = clone $this;
  453. $stm->startkey = $key;
  454. $stm->backward = false;
  455. $stm->just_key = $only_keys;
  456. return $stm;
  457. }
  458. function backward( $key = null, $only_keys = false )
  459. {
  460. assert('is_string($key) or is_null($key)');
  461. $stm = clone $this;
  462. $stm->startkey = $key;
  463. $stm->backward = true;
  464. $stm->just_key = $only_keys;
  465. return $stm;
  466. }
  467. function prefix( $prefix, $max = 0, &$num = null )
  468. {
  469. assert('is_string($prefix)');
  470. assert('is_numeric($max) and $max>=0 and (int)$max==$max');
  471. $stm = clone $this;
  472. $stm->prefix = $prefix;
  473. $stm->just_key = true;
  474. $stm->max = $max;
  475. $stm->num = &$num;
  476. return $stm;
  477. }
  478. function regex( $regex, $max = 0, &$num = null )
  479. {
  480. assert('is_string($regex)');
  481. assert('is_numeric($max) and $max>=0 and (int)$max==$max');
  482. $stm = clone $this;
  483. $stm->regex = $regex;
  484. $stm->just_key = true;
  485. $stm->max = $max;
  486. $stm->num = &$num;
  487. return $stm;
  488. }
  489. // }}}
  490. // {{{ rewind(), current(), key(), next(), valid()
  491. /**
  492. * TODO check if integer limit is reach with cursor.
  493. */
  494. function rewind()
  495. {
  496. // If prefix is set, then retrieve the list of keys begin with this prefix.
  497. if( ! is_null($this->prefix) )
  498. $this->keys = $this->backward
  499. ? array_reverse( $this->api->match_prefix( $this->prefix, $this->max, $this->num ) )
  500. : $this->api->match_prefix( $this->prefix, $this->max, $this->num );
  501. // Else, if regex is set, then retrieve the list of keys that match this regex.
  502. elseif( ! is_null($this->regex) )
  503. $this->keys = $this->backward
  504. ? array_reverse( $this->api->match_regex( $this->regex, $this->max, $this->num ) )
  505. : $this->api->match_regex( $this->regex, $this->max, $this->num );
  506. // Else, the cursor will be use
  507. else
  508. {
  509. // If no cursor was set, the create a new one. It need to be uniq for each cURL session.
  510. if( is_null($this->cursor) )
  511. {
  512. if( ! $cursor = end(self::$cursors) ) $this->cursor = 1;
  513. else $this->cursor = $cursor+1;
  514. self::$cursors[$this->cursor] = $this->cursor;
  515. }
  516. // Now set the position of the cursor.
  517. try
  518. {
  519. assert('is_bool($this->backward)');
  520. if( $this->backward )
  521. $this->api->cur_jump_back( $this->cursor, $this->startkey );
  522. else
  523. $this->api->cur_jump( $this->cursor, $this->startkey );
  524. }
  525. catch( OutOfBoundsException $e ) {}
  526. }
  527. }
  528. function current()
  529. {
  530. assert('is_array($this->record)');
  531. if( ! is_null($this->prefix) or ! is_null($this->regex) or ! is_null($this->cursor) )
  532. return current($this->record);
  533. else
  534. return null;
  535. }
  536. function key()
  537. {
  538. assert('is_array($this->record)');
  539. if( ! is_null($this->prefix) or ! is_null($this->regex) or ! is_null($this->cursor) )
  540. return key($this->record);
  541. else
  542. return null;
  543. }
  544. function next()
  545. {
  546. if( ! is_null($this->prefix) or ! is_null($this->regex) )
  547. {
  548. assert('is_array($this->keys)');
  549. next($this->keys);
  550. }
  551. elseif( ! is_null($this->cursor) )
  552. {
  553. try
  554. {
  555. if( $this->backward )
  556. $this->api->cur_step_back($this->cursor);
  557. else
  558. $this->api->cur_step($this->cursor);
  559. }
  560. catch( OutOfBoundsException $e ) {}
  561. }
  562. }
  563. function valid()
  564. {
  565. if( ! is_null($this->prefix) or ! is_null($this->regex) )
  566. {
  567. assert('is_array($this->keys)');
  568. if( current($this->keys) )
  569. try
  570. {
  571. if( $this->just_key )
  572. return $this->record = array( key($this->keys) => current($this->keys) );
  573. else
  574. return $this->record = array( current($this->keys) => $this->get(current($this->keys)) );
  575. }
  576. catch( OutOfBoundsException $e ) { return false; }
  577. else
  578. return false;
  579. }
  580. elseif( ! is_null($this->cursor) )
  581. {
  582. try {
  583. if( $this->just_key )
  584. return $this->record = array( $this->api->cur_get_key($this->cursor,false) );
  585. else
  586. return $this->record = $this->api->cur_get($this->cursor,false);
  587. }
  588. catch( OutOfBoundsException $e ) { return false; }
  589. }
  590. else
  591. return false;
  592. }
  593. // }}}
  594. // {{{ to(), from()
  595. function to( $key, &$value )
  596. {
  597. assert('is_string($key)');
  598. $value = $this->get($key);
  599. return $this;
  600. }
  601. function from( $key, &$value = null )
  602. {
  603. assert('is_string($key)');
  604. $this->set($key,$value);
  605. return $this;
  606. }
  607. // }}}
  608. // {{{ scr()
  609. function scr( $name, $data = null )
  610. {
  611. assert('is_string($name)');
  612. assert('is_array($data) or is_null($data)');
  613. try { return $this->api->play_script($name,$data); }
  614. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  615. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  616. }
  617. // }}}
  618. // {{{ offsetExists(), offsetGet(), offsetSet(), offsetUnset()
  619. function offsetExists( $offset )
  620. {
  621. assert('is_string($offset)');
  622. try { return is_string($this->api->get($offset)); }
  623. catch( OutOfBoundsException $e ) { return false; }
  624. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  625. }
  626. function offsetGet( $offset )
  627. {
  628. assert('is_string($offset)');
  629. try { return $this->api->get($offset); }
  630. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; else return null; }
  631. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; else return false; }
  632. }
  633. function offsetSet( $offset, $value )
  634. {
  635. assert('is_string($offset)');
  636. assert('is_string($value)');
  637. try { $this->api->set($offset,$value); }
  638. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; }
  639. }
  640. function offsetUnset( $offset )
  641. {
  642. assert('is_string($offset)');
  643. try { $this->api->remove($offset); }
  644. catch( OutOfBoundsException $e ) { if( $this->outofbound ) throw $e; }
  645. catch( RuntimeException $e ) { if( $this->runtime ) throw $e; }
  646. }
  647. // }}}
  648. }
  649. /**
  650. * The application programming interface (API) for KyotoTycoon.
  651. * Send RPC command with a keepalive connection.
  652. */
  653. final class API
  654. {
  655. // {{{ $keepalive, $timeout, $uri, $host, $post, $base, $encode, connect_to(), __construct()
  656. private $keepalive = 30;
  657. private $timeout = 3;
  658. // Contain all connection parameters in one URI.
  659. private $uri = null;
  660. function uri() { return $this->uri; }
  661. // The hostname or the IP of the server.
  662. private $host = null;
  663. function host() { return $this->host; }
  664. // The port of the server.
  665. private $port = null;
  666. function port() { return $this->port; }
  667. // The name or the ID of the database.
  668. private $base = null;
  669. function base() { return $this->base; }
  670. private $encode = null;
  671. function connect_to( $uri = 'http://localhost:1978' )
  672. {
  673. assert('is_array(parse_url($uri))');
  674. $this->host = parse_url( $uri, PHP_URL_HOST );
  675. $this->port = parse_url( $uri, PHP_URL_PORT );
  676. $this->base = trim( parse_url( $uri, PHP_URL_PATH ), '/' );
  677. $this->uri = "{$this->host}:{$this->port}";
  678. return $this;
  679. }
  680. function __construct( $uri = 'http://localhost:1978' )
  681. {
  682. assert('is_array(parse_url($uri))');
  683. $this->connect_to( $uri );
  684. $this->use_form_url();
  685. }
  686. // }}}
  687. // {{{ use_tab_base64(), use_tab_quoted(), use_tab_url(), use_tab(), use_form_url()
  688. function use_tab_base64()
  689. {
  690. $this->encode = function( $data )
  691. {
  692. assert('is_array($data)');
  693. return implode("\r\n", array_map( function($k,$v) {
  694. return sprintf("%s\t%s", base64_encode($k), base64_encode($v));
  695. }, array_keys($data), $data ));
  696. };
  697. curl_setopt($this->curl(), CURLOPT_HTTPHEADER, array('Content-type: text/tab-separated-values; colenc=B'));
  698. }
  699. function use_tab_quoted()
  700. {
  701. $this->encode = function( $data )
  702. {
  703. assert('is_array($data)');
  704. return implode("\r\n", array_map( function($k,$v) {
  705. return sprintf("%s\t%s", quoted_printable_encode($k), quoted_printable_encode($v));
  706. }, array_keys($data), $data ));
  707. };
  708. curl_setopt($this->curl(), CURLOPT_HTTPHEADER, array('Content-type: text/tab-separated-values; colenc=Q'));
  709. }
  710. function use_tab_url()
  711. {
  712. $this->encode = function( $data )
  713. {
  714. assert('is_array($data)');
  715. return implode("\r\n", array_map( function($k,$v) {
  716. return sprintf("%s\t%s", urlencode($k), urlencode($v));
  717. }, array_keys($data), $data ));
  718. };
  719. curl_setopt($this->curl(), CURLOPT_HTTPHEADER, array('Content-type: text/tab-separated-values; colenc=U'));
  720. }
  721. function use_tab()
  722. {
  723. $this->encode = function( $data )
  724. {
  725. assert('is_array($data)');
  726. return implode("\r\n", array_map( function($k,$v) {
  727. return sprintf("%s\t%s", str_replace($k,"\r\n\t",''), str_replace($v,"\r\n\t",''));
  728. }, array_keys($data), $data ));
  729. };
  730. curl_setopt($this->curl(), CURLOPT_HTTPHEADER, array('Content-type: text/tab-separated-values'));
  731. }
  732. function use_form_url()
  733. {
  734. $this->encode = function( $data )
  735. {
  736. assert('is_array($data)');
  737. return http_build_query($data);
  738. };
  739. curl_setopt($this->curl(), CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded'));
  740. }
  741. // }}}
  742. // {{{ decode_tab, decode_tab_url, decode_tab_base64, decode_tab_quoted, decode_form_url
  743. static private function decode_tab( $data )
  744. {
  745. $result = array();
  746. $length = strlen($data);
  747. $offset = 0;
  748. while( $offset < $length
  749. and $key = strpos($data,"\t",$offset)
  750. and ($val = strpos($data,"\n",$key) or $val = $length) )
  751. {
  752. $result[substr($data,$offset,$key-$offset)]
  753. = substr($data,$key+1,$val-$key-1);
  754. $offset = $val+1;
  755. }
  756. return $result;
  757. }
  758. static private function decode_tab_url( $data )
  759. {
  760. $result = array();
  761. $length = strlen($data);
  762. $offset = 0;
  763. while( $offset < $length
  764. and $key = strpos($data,"\t",$offset)
  765. and ($val = strpos($data,"\n",$key) or $val = $length) )
  766. {
  767. $result[urldecode(substr($data,$offset,$key-$offset))]
  768. = urldecode(substr($data,$key+1,$val-$key-1));
  769. $offset = $val+1;
  770. }
  771. return $result;
  772. }
  773. static private function decode_tab_quoted( $data )
  774. {
  775. $result = array();
  776. $length = strlen($data);
  777. $offset = 0;
  778. while( $offset < $length
  779. and $key = strpos($data,"\t",$offset)
  780. and ($val = strpos($data,"\n",$key) or $val = $length) )
  781. {
  782. $result[quoted_printable_decode(substr($data,$offset,$key-$offset))]
  783. = quoted_printable_decode(substr($data,$key+1,$val-$key-1));
  784. $offset = $val+1;
  785. }
  786. return $result;
  787. }
  788. static private function decode_tab_base64( $data )
  789. {
  790. $result = array();
  791. $length = strlen($data);
  792. $offset = 0;
  793. while( $offset < $length
  794. and $key = strpos($data,"\t",$offset)
  795. and ($val = strpos($data,"\n",$key) or $val = $length) )
  796. {
  797. $result[base64_decode(substr($data,$offset,$key-$offset))]
  798. = base64_decode(substr($data,$key+1,$val-$key-1));
  799. $offset = $val+1;
  800. }
  801. return $result;
  802. }
  803. static private function decode_form_url( $data )
  804. {
  805. return extract($data,true);
  806. }
  807. // }}}
  808. // {{{ add()
  809. /**
  810. * Add a record.
  811. * Params:
  812. * string $key = The key of the record.
  813. * string $value = The value of the record.
  814. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  815. * null $xt = No expiration time is specified.
  816. * Return:
  817. * true = If success
  818. * Throws:
  819. * InconsistencyException = If the record already exists.
  820. */
  821. function add( $key, $value, $xt = null )
  822. {
  823. assert('is_string($key)');
  824. assert('is_string($value)');
  825. assert('is_null($xt) or is_numeric($xt)');
  826. if( $this->base ) $DB = $this->base;
  827. if( ! $xt ) unset($xt); else $xt = (string)$xt;
  828. return $this->rpc( 'add', compact('DB','key','value','xt'), null );
  829. }
  830. // }}}
  831. // {{{ append()
  832. /**
  833. * Append the value to a record.
  834. * Params:
  835. * string $key = The key of the record.
  836. * string $value = The value of the record.
  837. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  838. * null $xt = No expiration time is specified.
  839. * Return:
  840. * true = If success
  841. */
  842. function append( $key, $value, $xt = null )
  843. {
  844. assert('is_string($key)');
  845. assert('is_string($value)');
  846. assert('is_null($xt) or is_numeric($xt)');
  847. if( $this->base ) $DB = $this->base;
  848. if( ! $xt ) unset($xt); else $xt = (string)$xt;
  849. return $this->rpc( 'append', compact('DB','key','value','xt'), null );
  850. }
  851. // }}}
  852. // {{{ cas()
  853. /**
  854. * Perform compare-and-swap.
  855. * Params:
  856. * string $key = The key of the record.
  857. * string $oval = The old value.
  858. * null $oval = If it is omittted, no record is meant.
  859. * string $nval = The new value.
  860. * null $nval = If it is omittted, the record is removed.
  861. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  862. * null $xt = No expiration time is specified.
  863. * Return:
  864. * true = If success
  865. */
  866. function cas( $key, $oval, $nval, $xt = null )
  867. {
  868. assert('is_string($key)');
  869. assert('is_string($oval) or is_null($oval)');
  870. assert('is_string($nval) or is_null($nval)');
  871. assert('is_null($xt) or is_numeric($xt)');
  872. if( $this->base ) $DB = $this->base;
  873. if( ! $xt ) unset($xt); else $xt = (string)$xt;
  874. if( ! $oval ) unset($oval);
  875. if( ! $nval ) unset($nval);
  876. return $this->rpc( 'cas', compact('DB','key','oval','nval','xt'), null );
  877. }
  878. // }}}
  879. // {{{ clear
  880. function __get( $property )
  881. {
  882. assert('preg_match("/^[\w_]+$/",$property)');
  883. switch( $property )
  884. {
  885. case 'clear':
  886. if( $this->base ) $DB = $this->base;
  887. return $this->rpc( 'clear', compact('DB'), null );
  888. }
  889. }
  890. // }}}
  891. // {{{ get(), getful()
  892. /**
  893. * Retrieve the value of a record.
  894. * Params:
  895. * string $key = The key of the record.
  896. * (out) integer $xt = The absolute expiration time.
  897. * (out) null $xt = There is no expiration time.
  898. * Return:
  899. * string = The value of the record.
  900. * Throws:
  901. * InconsistencyException = If the record do not exists.
  902. */
  903. function get( $key, &$xt = null )
  904. {
  905. assert('is_string($key)');
  906. if( $this->base ) $DB = $this->base;
  907. if( ! $xt ) unset($xt); else $xt = (string)$xt;
  908. return $this->rpc( 'get', compact('DB','key'), function($result) use(&$xt) {
  909. if( isset($result['xt']) ) $xt = $result['xt'];
  910. if( isset($result['value']) )
  911. // fixme: delete me return $result['value']?$result['value']:"";
  912. return $result['value'];
  913. else
  914. throw new ProtocolException( $this->url );
  915. } );
  916. }
  917. function getful( $key, &$xt = null, &$time = null )
  918. {
  919. assert('is_string($key)');
  920. return $this->rest( 'GET', $key, null, function($headers) use(&$xt,&$time) {
  921. if( isset($headers['X-Kt-Xt']) ) $xt = $headers['X-Kt-Xt'];
  922. if( isset($headers['Date']) ) $time = $headers['Date'];
  923. } );
  924. }
  925. // }}}
  926. // {{{ cur_get()
  927. /**
  928. * Get a pair of the key and the value of the current record.
  929. * Params:
  930. * integer $CUR = The cursor identifier.
  931. * true $step = To move the cursor to the next record.
  932. * null,false $step = If it is omitted, the cursor stays at the current record.
  933. * Return:
  934. * array(string=>string) = The key and the value of the record.
  935. * Throws:
  936. * InconsistencyException = If the cursor is invalidated.
  937. */
  938. function cur_get( $CUR, $step = true )
  939. {
  940. assert('is_integer($CUR)');
  941. assert('is_bool($step) or is_null($step)');
  942. if( ! $step ) unset($step); else $step = (string)$step;
  943. $CUR = (string)$CUR;
  944. return $this->rpc( 'cur_get', compact('CUR','step'), function($result) {
  945. return array($result['key']=>$result['value']);
  946. } );
  947. }
  948. // }}}
  949. // {{{ cur_get_key()
  950. /**
  951. * Get the key of the current record.
  952. * Params:
  953. * integer $CUR = The cursor identifier.
  954. * true $step = To move the cursor to the next record.
  955. * null,false $step = If it is omitted, the cursor stays at the current record.
  956. * Return:
  957. * string = The key of the record.
  958. * Throws:
  959. * InconsistencyException = If the cursor is invalidated.
  960. */
  961. function cur_get_key( $CUR, $step = true )
  962. {
  963. assert('is_integer($CUR)');
  964. assert('is_bool($step) or is_null($step)');
  965. if( ! $step ) unset($step); else $step = (string)$step;
  966. $CUR = (string)$CUR;
  967. return $this->rpc( 'cur_get_key', compact('CUR','step'), function($result) {
  968. return $result['key'];
  969. } );
  970. }
  971. // }}}
  972. // {{{ cur_get_value()
  973. /**
  974. * Get a pair of the key and the value of the current record.
  975. * Params:
  976. * integer $CUR = The cursor identifier.
  977. * true $step = To move the cursor to the next record.
  978. * null,false $step = If it is omitted, the cursor stays at the current record.
  979. * Return:
  980. * string = The value of the record.
  981. * Throws:
  982. * InconsistencyException = If the cursor is invalidated.
  983. */
  984. function cur_get_value( $CUR, $step = true )
  985. {
  986. assert('is_integer($CUR)');
  987. assert('is_bool($step) or is_null($step)');
  988. if( ! $step ) unset($step); else $step = (string)$step;
  989. $CUR = (string)$CUR;
  990. return $this->rpc( 'cur_get_value', compact('CUR','step'), function($result) {
  991. return $result['value'];
  992. } );
  993. }
  994. // }}}
  995. // {{{ cur_jump()
  996. /**
  997. * Jump the cursor to the first record for forward scan.
  998. * Params:
  999. * integer $CUR = The cursor identifier.
  1000. * string $key = The key of the destination record.
  1001. * null $key = If it is omitted, the first record is specified.
  1002. * Return:
  1003. * true = If success
  1004. * Throws:
  1005. * InconsistencyException = If the cursor is invalidated.
  1006. */
  1007. function cur_jump( $CUR, $key = null )
  1008. {
  1009. assert('is_integer($CUR)');
  1010. assert('is_string($key) or is_null($key)');
  1011. if( $this->base ) $DB = $this->base;
  1012. if( ! $key ) unset($key);
  1013. $CUR = (string)$CUR;
  1014. return $this->rpc( 'cur_jump', compact('DB','CUR','key'), null );
  1015. }
  1016. // }}}
  1017. // {{{ cur_jump_back()
  1018. /**
  1019. * Jump the cursor to a record for forward scan.
  1020. * Params:
  1021. * integer $CUR = The cursor identifier.
  1022. * string $key = The key of the destination record.
  1023. * null $key = If it is omitted, the first record is specified.
  1024. * Return:
  1025. * true = If success
  1026. * Throws:
  1027. * InconsistencyException = If the cursor is invalidated.
  1028. */
  1029. function cur_jump_back( $CUR, $key = null )
  1030. {
  1031. assert('is_integer($CUR)');
  1032. assert('is_string($key) or is_null($key)');
  1033. if( $this->base ) $DB = $this->base;
  1034. if( ! $key ) unset($key);
  1035. $CUR = (string)$CUR;
  1036. return $this->rpc( 'cur_jump_back', compact('DB','CUR','key'), null );
  1037. }
  1038. // }}}
  1039. // {{{ cur_step()
  1040. /**
  1041. * Retrieve the value of a record.
  1042. * Params:
  1043. * integer $CUR = The cursor identifier.
  1044. * Return:
  1045. * true = If success
  1046. * Throws:
  1047. * InconsistencyException = If the cursor is invalidated.
  1048. */
  1049. function cur_step( $CUR )
  1050. {
  1051. assert('is_integer($CUR)');
  1052. $CUR = (string)$CUR;
  1053. return $this->rpc( 'cur_step', compact('CUR'), null );
  1054. }
  1055. // }}}
  1056. // {{{ cur_step_back()
  1057. /**
  1058. * Retrieve the value of a record.
  1059. * Params:
  1060. * integer $CUR = The cursor identifier.
  1061. * Return:
  1062. * true = If success
  1063. * Throws:
  1064. * InconsistencyException = If the cursor is invalidated.
  1065. */
  1066. function cur_step_back( $CUR )
  1067. {
  1068. assert('is_integer($CUR)');
  1069. $CUR = (string)$CUR;
  1070. return $this->rpc( 'cur_step_back', compact('CUR'), null );
  1071. }
  1072. // }}}
  1073. // {{{ cur_remove()
  1074. /**
  1075. * Remove the current record.
  1076. * Params:
  1077. * integer $CUR = The cursor identifier.
  1078. * Return:
  1079. * true = If success
  1080. * Throws:
  1081. * InconsistencyException = If the cursor is invalidated.
  1082. */
  1083. function cur_remove( $CUR )
  1084. {
  1085. assert('is_integer($CUR)');
  1086. $CUR = (string)$CUR;
  1087. return $this->rpc( 'cur_remove', compact('CUR'), null );
  1088. }
  1089. // }}}
  1090. // {{{ increment()
  1091. /**
  1092. * Add a number to the numeric integer value of a record.
  1093. * Params:
  1094. * string $key = The key of the record.
  1095. * numeric $num = The additional number.
  1096. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  1097. * null $xt = No expiration time is specified.
  1098. * Return:
  1099. * string = The result value.
  1100. * Throws:
  1101. * InconsistencyException = If the record was not compatible.
  1102. */
  1103. function increment( $key, $num = 1, $xt = null )
  1104. {
  1105. assert('is_string($key)');
  1106. assert('is_numeric($num)');
  1107. assert('is_null($xt) or is_numeric($xt)');
  1108. if( $this->base ) $DB = $this->base;
  1109. if( ! $xt ) unset($xt); else $xt = (string)$xt;
  1110. $num = (string)$num;
  1111. return $this->rpc( 'increment', compact('DB','key','num','xt'), function($result) use(&$xt) {
  1112. return $result['num'];
  1113. } );
  1114. }
  1115. // }}}
  1116. // {{{ increment_double()
  1117. /**
  1118. * Add a number to the numeric integer value of a record.
  1119. * Params:
  1120. * string $key = The key of the record.
  1121. * numeric $num = The additional number.
  1122. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  1123. * null $xt = No expiration time is specified.
  1124. * Return:
  1125. * string = The result value.
  1126. * Throws:
  1127. * InconsistencyException = If the record was not compatible.
  1128. */
  1129. function increment_double( $key, $num = 1, $xt = null )
  1130. {
  1131. assert('is_string($key)');
  1132. assert('is_numeric($num)');
  1133. assert('is_null($xt) or is_numeric($xt)');
  1134. if( $this->base ) $DB = $this->base;
  1135. if( ! $xt ) unset($xt); else $xt = (string)$xt;
  1136. $num = (string)$num;
  1137. return $this->rpc( 'increment_double', compact('DB','key','num','xt'), function($result) use(&$xt) {
  1138. return $result['num'];
  1139. } );
  1140. }
  1141. // }}}
  1142. // {{{ match_prefix()
  1143. /**
  1144. * Get keys matching a prefix string.
  1145. * Params:
  1146. * string $prefix = The prefix string.
  1147. * integer $max = The maximum number to retrieve.
  1148. * null $max = If it is omitted or negative, no limit is specified.
  1149. * (out) $num = The number of retrieved keys.
  1150. * Return:
  1151. * array(string) = List of arbitrary keys.
  1152. * Throws:
  1153. * InconsistencyException = If the record do not exists.
  1154. */
  1155. function match_prefix( $prefix, $max = null, $num = null )
  1156. {
  1157. assert('is_string($prefix)');
  1158. assert('is_numeric($max) or is_null($max)');
  1159. if( $this->base ) $DB = $this->base;
  1160. if( ! $max ) unset($max); else $max = (string)$max;
  1161. return $this->rpc( 'match_prefix', compact('DB','prefix','max'), function($result) use(&$num) {
  1162. $num = $result['num'];
  1163. return array_reduce(array_keys($result),function($a,$b){return $b[0]=='_'?array_merge($a,array(substr($b,1))):$a;},array());
  1164. } );
  1165. }
  1166. // }}}
  1167. // {{{ match_regex()
  1168. /**
  1169. * Get keys matching a ragular expression string.
  1170. * Params:
  1171. * string $regex = The regular expression string.
  1172. * integer $max = The maximum number to retrieve.
  1173. * null $max = If it is omitted or negative, no limit is specified.
  1174. * (out) string $num = The number of retrieved keys.
  1175. * Return:
  1176. * array(string) = List of arbitrary keys.
  1177. * Throws:
  1178. * InconsistencyException = If the record do not exists.
  1179. */
  1180. function match_regex( $regex, $max = null, $num = null )
  1181. {
  1182. assert('is_string($regex)');
  1183. assert('is_numeric($max) or is_null($max)');
  1184. if( $this->base ) $DB = $this->base;
  1185. if( ! $max ) unset($max); else $max = (string)$max;
  1186. return $this->rpc( 'match_regex', compact('DB','regex','max'), function($result) use(&$num) {
  1187. $num = $result['num'];
  1188. return array_reduce(array_keys($result),function($a,$b){return $b[0]=='_'?array_merge($a,array(substr($b,1))):$a;},array());
  1189. } );
  1190. }
  1191. // }}}
  1192. // {{{ play_script()
  1193. function play_script( $name, $data = null )
  1194. {
  1195. assert('is_string($name)');
  1196. assert('is_array($data) or is_null($data)');
  1197. return $this->rpc( 'play_script', array_merge(compact('name'),$data?array_reduce(array_keys($data),function($a,$b)use(&$data){return array_merge($a,array("_$b"=>$data[$b]));},array()):array()), function($result) {
  1198. return array_reduce(array_keys($result),function($a,$b)use(&$result){return $b[0]=='_'?array_merge($a,array(substr($b,1)=>$result[$b])):$a;},array());
  1199. } );
  1200. }
  1201. // }}}
  1202. // {{{ remove()
  1203. /**
  1204. * Replace the value of a record.
  1205. * Params:
  1206. * string $key = The key of the record.
  1207. * Return:
  1208. * true = If success
  1209. * Throws:
  1210. * InconsistencyException = If the record do not exists.
  1211. */
  1212. function remove( $key )
  1213. {
  1214. assert('is_string($key)');
  1215. if( $this->base ) $DB = $this->base;
  1216. return $this->rpc( 'remove', compact('DB','key'), null );
  1217. }
  1218. // }}}
  1219. // {{{ replace()
  1220. /**
  1221. * Replace the value of a record.
  1222. * Params:
  1223. * string $key = The key of the record.
  1224. * string $value = The value of the record.
  1225. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  1226. * null $xt = No expiration time is specified.
  1227. * Return:
  1228. * true = If success
  1229. * Throws:
  1230. * InconsistencyException = If the record do not exists.
  1231. */
  1232. function replace( $key, $value, $xt = null )
  1233. {
  1234. assert('is_string($key)');
  1235. assert('is_string($value)');
  1236. assert('is_null($xt) or is_numeric($xt)');
  1237. if( $this->base ) $DB = $this->base;
  1238. if( ! $xt ) unset($xt); else $xt = (string)$xt;
  1239. return $this->rpc( 'replace', compact('DB','key','value','xt'), null );
  1240. }
  1241. // }}}
  1242. // {{{ set()
  1243. /**
  1244. * Set the value of a record.
  1245. * Params:
  1246. * string $key = The key of the record.
  1247. * string $value = The value of the record.
  1248. * numeric $xt = The expiration time from now in seconds. If it is negative, the absolute value is treated as the epoch time.
  1249. * null $xt = No expiration time is specified.
  1250. * Return:
  1251. * true = If success
  1252. */
  1253. function set( $key, $value, $xt = null )
  1254. {
  1255. assert('is_string($key)');
  1256. assert('is_string($value)');
  1257. assert('is_null($xt) or is_numeric($xt)');
  1258. if( $this->base ) $DB = $this->base;
  1259. if( ! $xt ) unset($xt); else $xt = (string)$xt;
  1260. return $this->rpc( 'set', compact('DB','key','value','xt'), null );
  1261. }
  1262. // }}}
  1263. // {{{ curl(), rpc(), rest()
  1264. /**
  1265. * Return a curl resource identifier
  1266. * KyotoTycoon use a keep-alive connection by default.
  1267. */
  1268. private function curl()
  1269. {
  1270. static $curl = null;
  1271. if( is_null($curl) )
  1272. {
  1273. $curl = curl_init();
  1274. curl_setopt_array($curl, array(
  1275. //CURLOPT_VERBOSE => true,
  1276. CURLOPT_RETURNTRANSFER => true,
  1277. CURLOPT_CONNECTTIMEOUT => $this->timeout,
  1278. CURLOPT_TIMEOUT => $this->keepalive ));
  1279. }
  1280. return $curl;
  1281. }
  1282. /**
  1283. * Send an RPC command to a KyotoTycoon server.
  1284. * Params:
  1285. * string $cmd = The command.
  1286. * array,null $data = Lexical indexed array containing the input parameters.
  1287. * $return callable($result) = $when_ok = A callback function called if success.
  1288. * array $result = Lexical indexed array containing the output parameters.
  1289. * string,false $return = The returned value of the command or true if success.
  1290. * Return:
  1291. *
  1292. */
  1293. private function rpc( $cmd, $data = null, $when_ok = null )
  1294. {
  1295. static $encode = null; if( is_null($encode) ) $encode = &$this->encode;
  1296. assert('in_array($cmd,array("add","append","cas","clear","cur_delete","cur_get","cur_get_key","cur_get_value","cur_jump","cur_jump_back","cur_set_value","cur_step","cur_step_back","cur_remove","echo","get","get_bulk","increment","increment_double","match_prefix","match_regex","play_script","remove","remove_bulk","replace","report","set","set_bulk","status","synchronize","tune_replication","vacuum"))');
  1297. assert('is_null($data) or is_array($data)');
  1298. assert('!$data or array_walk($data,function($v,$k){assert(\'is_string($k)\');assert(\'is_string($v)\');})');
  1299. assert('is_null($data) or count($data)==count(array_filter(array_keys($data),"is_string"))');
  1300. assert('is_null($data) or count($data)==count(array_filter($data,"is_string"))');
  1301. assert('is_callable($when_ok) or is_null($when_ok)');
  1302. if( is_array($data) )
  1303. $post = $encode($data);
  1304. else
  1305. $post = '';
  1306. unset($data);
  1307. assert('is_string($post)');
  1308. curl_setopt_array($this->curl(), array(
  1309. CURLOPT_URL => "{$this->uri}/rpc/{$cmd}",
  1310. CURLOPT_HEADER => false,
  1311. CURLOPT_POST => true,
  1312. CURLOPT_POSTFIELDS => $post ));
  1313. if( is_string($data = curl_exec($this->curl())) and $data ) switch( curl_getinfo($this->curl(),CURLINFO_CONTENT_TYPE) )
  1314. {
  1315. case 'text/tab-separated-values':
  1316. $data = self::decode_tab($data); break;
  1317. case 'text/tab-separated-values; colenc=B':
  1318. $data = self::decode_tab_base64($data); break;
  1319. case 'text/tab-separated-values; colenc=U':
  1320. $data = self::decode_tab_url($data); break;
  1321. default: var_dump(curl_getinfo($this->curl(),CURLINFO_CONTENT_TYPE));throw new ProtocolException($this->uri);
  1322. }
  1323. elseif( $data === false )
  1324. throw new ConnectionException($this->uri, curl_error($this->curl()));
  1325. else
  1326. $data = array();
  1327. switch( curl_getinfo($this->curl(),CURLINFO_HTTP_CODE) )
  1328. {
  1329. case 200:
  1330. if( $when_ok )
  1331. {
  1332. $data = call_user_func( $when_ok, $data );
  1333. assert('is_string($data) or is_array($data) or $data===true');
  1334. return $data;
  1335. }
  1336. else
  1337. return true;
  1338. case 450: throw new InconsistencyException($this->uri,$data['ERROR']);
  1339. case 501: throw new ImplementationException($this->uri);
  1340. case 400: throw new ProtocolException($this->uri);
  1341. }
  1342. }
  1343. // }}}
  1344. public function rest( $cmd, $key, $prepare = null, $when_ok = null )
  1345. {
  1346. assert('in_array($cmd,array("GET","HEAD","PUT","DELETE"))');
  1347. assert('is_string($key)');
  1348. assert('is_null($prepare) or $prepare instanceof Closure');
  1349. assert('is_null($when_ok) or $when_ok instanceof Closure');
  1350. curl_setopt_array($this->curl(), array(
  1351. CURLOPT_HEADER => true,
  1352. CURLOPT_URL => "{$this->uri}/".urlencode($key),
  1353. $cmd=='HEAD' ? CURLOPT_NOBODY : CURLOPT_POST => $cmd=='HEAD' ? true : false ));
  1354. if( $prepare ) $prepare($this->curl());
  1355. $headers = curl_exec($this->curl());
  1356. if( false !==($tmp = strpos($headers,"\r\n\r\n")) )
  1357. {
  1358. $data = substr($headers,$tmp+4);
  1359. $headers = substr($headers,0,$tmp);
  1360. }
  1361. else
  1362. $data = '';
  1363. switch( curl_getinfo($this->curl(),CURLINFO_HTTP_CODE) )
  1364. {
  1365. case 200:
  1366. if( $when_ok ) call_user_func( $when_ok, array_reduce(explode("\r\n",$headers),function($a,$v) {
  1367. if( false !== ($tmp = strpos($v,': ')) )
  1368. return array_merge($a,array(substr($v,0,$tmp)=>substr($v,$tmp+2)));
  1369. else
  1370. return $a;
  1371. },array()) );
  1372. return $data;
  1373. case 404: throw new InconsistencyException($this->uri,'No record was found');
  1374. case 501: throw new ImplementationException($this->uri);
  1375. case 400: throw new ProtocolException($this->uri);
  1376. }
  1377. }
  1378. }