WebdisConnection.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. <?php
  2. /*
  3. * This file is part of the Predis package.
  4. *
  5. * (c) Daniele Alessandri <suppakilla@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Predis\Network;
  11. use Predis\IConnectionParameters;
  12. use Predis\ResponseError;
  13. use Predis\ClientException;
  14. use Predis\ServerException;
  15. use Predis\Commands\ICommand;
  16. use Predis\Protocol\ProtocolException;
  17. const ERR_MSG_EXTENSION = 'The %s extension must be loaded in order to be able to use this connection class';
  18. /**
  19. * This class implements a Predis connection that actually talks with Webdis
  20. * instead of connecting directly to Redis. It relies on the cURL extension to
  21. * communicate with the web server and the phpiredis extension to parse the
  22. * protocol of the replies returned in the http response bodies.
  23. *
  24. * Some features are not yet available or they simply cannot be implemented:
  25. * - Pipelining commands.
  26. * - Publish / Subscribe.
  27. * - MULTI / EXEC transactions (not yet supported by Webdis).
  28. *
  29. * @link http://webd.is
  30. * @link http://github.com/nicolasff/webdis
  31. * @link http://github.com/seppo0010/phpiredis
  32. * @author Daniele Alessandri <suppakilla@gmail.com>
  33. */
  34. class WebdisConnection implements IConnectionSingle
  35. {
  36. private $parameters;
  37. private $resource;
  38. private $reader;
  39. /**
  40. * @param IConnectionParameters $parameters Parameters used to initialize the connection.
  41. */
  42. public function __construct(IConnectionParameters $parameters)
  43. {
  44. $this->parameters = $parameters;
  45. if ($parameters->scheme !== 'http') {
  46. throw new \InvalidArgumentException("Invalid scheme: {$parameters->scheme}");
  47. }
  48. $this->checkExtensions();
  49. $this->resource = $this->initializeCurl($parameters);
  50. $this->reader = $this->initializeReader($parameters);
  51. }
  52. /**
  53. * Frees the underlying cURL and protocol reader resources when PHP's
  54. * garbage collector kicks in.
  55. */
  56. public function __destruct()
  57. {
  58. curl_close($this->resource);
  59. phpiredis_reader_destroy($this->reader);
  60. }
  61. /**
  62. * Helper method used to throw on unsupported methods.
  63. */
  64. private function throwNotSupportedException($function)
  65. {
  66. $class = __CLASS__;
  67. throw new \RuntimeException("The method $class::$function() is not supported");
  68. }
  69. /**
  70. * Checks if the cURL and phpiredis extensions are loaded in PHP.
  71. */
  72. private function checkExtensions()
  73. {
  74. if (!function_exists('curl_init')) {
  75. throw new ClientException(sprintf(ERR_MSG_EXTENSION, 'curl'));
  76. }
  77. if (!function_exists('phpiredis_reader_create')) {
  78. throw new ClientException(sprintf(ERR_MSG_EXTENSION, 'phpiredis'));
  79. }
  80. }
  81. /**
  82. * Initializes cURL.
  83. *
  84. * @param IConnectionParameters $parameters Parameters used to initialize the connection.
  85. * @return resource
  86. */
  87. private function initializeCurl(IConnectionParameters $parameters)
  88. {
  89. $options = array(
  90. CURLOPT_FAILONERROR => true,
  91. CURLOPT_CONNECTTIMEOUT_MS => $parameters->connection_timeout * 1000,
  92. CURLOPT_URL => "{$parameters->scheme}://{$parameters->host}:{$parameters->port}",
  93. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  94. CURLOPT_POST => true,
  95. CURLOPT_WRITEFUNCTION => array($this, 'feedReader'),
  96. );
  97. if (isset($parameters->user, $parameters->pass)) {
  98. $options[CURLOPT_USERPWD] = "{$parameters->user}:{$parameters->pass}";
  99. }
  100. $resource = curl_init();
  101. curl_setopt_array($resource, $options);
  102. return $resource;
  103. }
  104. /**
  105. * Initializes phpiredis' protocol reader.
  106. *
  107. * @param IConnectionParameters $parameters Parameters used to initialize the connection.
  108. * @return resource
  109. */
  110. private function initializeReader(IConnectionParameters $parameters)
  111. {
  112. $reader = phpiredis_reader_create();
  113. phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
  114. phpiredis_reader_set_error_handler($reader, $this->getErrorHandler($parameters->throw_errors));
  115. return $reader;
  116. }
  117. /**
  118. * Gets the handler used by the protocol reader to handle status replies.
  119. *
  120. * @return \Closure
  121. */
  122. private function getStatusHandler()
  123. {
  124. return function($payload) {
  125. return $payload === 'OK' ? true : $payload;
  126. };
  127. }
  128. /**
  129. * Gets the handler used by the protocol reader to handle Redis errors.
  130. *
  131. * @param Boolean $throwErrors Specify if Redis errors throw exceptions.
  132. * @return \Closure
  133. */
  134. private function getErrorHandler($throwErrors)
  135. {
  136. if ($throwErrors) {
  137. return function($errorMessage) {
  138. throw new ServerException($errorMessage);
  139. };
  140. }
  141. return function($errorMessage) {
  142. return new ResponseError($errorMessage);
  143. };
  144. }
  145. /**
  146. * Feeds phpredis' reader resource with the data read from the network.
  147. *
  148. * @param resource $resource Reader resource.
  149. * @param string $buffer Buffer with the reply read from the network.
  150. * @return int
  151. */
  152. protected function feedReader($resource, $buffer)
  153. {
  154. phpiredis_reader_feed($this->reader, $buffer);
  155. return strlen($buffer);
  156. }
  157. /**
  158. * {@inheritdoc}
  159. */
  160. public function connect()
  161. {
  162. // NOOP
  163. }
  164. /**
  165. * {@inheritdoc}
  166. */
  167. public function disconnect()
  168. {
  169. // NOOP
  170. }
  171. /**
  172. * {@inheritdoc}
  173. */
  174. public function isConnected()
  175. {
  176. return true;
  177. }
  178. /**
  179. * Checks if the specified command is supported by this connection class.
  180. *
  181. * @param ICommand $command The instance of a Redis command.
  182. * @return string
  183. */
  184. protected function getCommandId(ICommand $command)
  185. {
  186. switch (($commandId = $command->getId())) {
  187. case 'AUTH':
  188. case 'SELECT':
  189. case 'MULTI':
  190. case 'EXEC':
  191. case 'WATCH':
  192. case 'UNWATCH':
  193. case 'DISCARD':
  194. throw new \InvalidArgumentException("Disabled command: {$command->getId()}");
  195. default:
  196. return $commandId;
  197. }
  198. }
  199. /**
  200. * {@inheritdoc}
  201. */
  202. public function writeCommand(ICommand $command)
  203. {
  204. $this->throwNotSupportedException(__FUNCTION__);
  205. }
  206. /**
  207. * {@inheritdoc}
  208. */
  209. public function readResponse(ICommand $command)
  210. {
  211. $this->throwNotSupportedException(__FUNCTION__);
  212. }
  213. /**
  214. * {@inheritdoc}
  215. */
  216. public function executeCommand(ICommand $command)
  217. {
  218. $resource = $this->resource;
  219. $commandId = $this->getCommandId($command);
  220. if ($arguments = $command->getArguments()) {
  221. $arguments = implode('/', array_map('urlencode', $arguments));
  222. $serializedCommand = "$commandId/$arguments.raw";
  223. }
  224. else {
  225. $serializedCommand = "$commandId.raw";
  226. }
  227. curl_setopt($resource, CURLOPT_POSTFIELDS, $serializedCommand);
  228. if (curl_exec($resource) === false) {
  229. $error = curl_error($resource);
  230. $errno = curl_errno($resource);
  231. throw new ConnectionException($this, trim($error), $errno);
  232. }
  233. $readerState = phpiredis_reader_get_state($this->reader);
  234. if ($readerState === PHPIREDIS_READER_STATE_COMPLETE) {
  235. $reply = phpiredis_reader_get_reply($this->reader);
  236. if ($reply instanceof IReplyObject) {
  237. return $reply;
  238. }
  239. return $command->parseResponse($reply);
  240. }
  241. else {
  242. $error = phpiredis_reader_get_error($this->reader);
  243. throw new ProtocolException($this, $error);
  244. }
  245. }
  246. /**
  247. * {@inheritdoc}
  248. */
  249. public function getResource()
  250. {
  251. return $this->resource;
  252. }
  253. /**
  254. * {@inheritdoc}
  255. */
  256. public function getParameters()
  257. {
  258. return $this->parameters;
  259. }
  260. /**
  261. * {@inheritdoc}
  262. */
  263. public function pushInitCommand(ICommand $command)
  264. {
  265. $this->throwNotSupportedException(__FUNCTION__);
  266. }
  267. /**
  268. * {@inheritdoc}
  269. */
  270. public function read()
  271. {
  272. $this->throwNotSupportedException(__FUNCTION__);
  273. }
  274. /**
  275. * {@inheritdoc}
  276. */
  277. public function __toString()
  278. {
  279. return "{$this->parameters->host}:{$this->parameters->port}";
  280. }
  281. }