WebdisConnection.php 9.1 KB

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