CurlTransport.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\httpclient;
  8. use Yii;
  9. /**
  10. * CurlTransport sends HTTP messages using [Client URL Library (cURL)](http://php.net/manual/en/book.curl.php)
  11. *
  12. * Note: this transport requires PHP 'curl' extension installed.
  13. *
  14. * For this transport, you may setup request options as [cURL Options](http://php.net/manual/en/function.curl-setopt.php)
  15. *
  16. * @author Paul Klimov <klimov.paul@gmail.com>
  17. * @since 2.0
  18. */
  19. class CurlTransport extends Transport
  20. {
  21. /**
  22. * @inheritdoc
  23. */
  24. public function send($request)
  25. {
  26. $request->beforeSend();
  27. $curlOptions = $this->prepare($request);
  28. $curlResource = $this->initCurl($curlOptions);
  29. $responseHeaders = [];
  30. $this->setHeaderOutput($curlResource, $responseHeaders);
  31. $token = $request->client->createRequestLogToken($request->getMethod(), $curlOptions[CURLOPT_URL], $curlOptions[CURLOPT_HTTPHEADER], $request->getContent());
  32. Yii::info($token, __METHOD__);
  33. Yii::beginProfile($token, __METHOD__);
  34. try {
  35. $responseContent = curl_exec($curlResource);
  36. } catch (\Exception $e) {
  37. Yii::endProfile($token, __METHOD__);
  38. throw new Exception($e->getMessage(), $e->getCode(), $e);
  39. }
  40. Yii::endProfile($token, __METHOD__);
  41. // check cURL error
  42. $errorNumber = curl_errno($curlResource);
  43. $errorMessage = curl_error($curlResource);
  44. curl_close($curlResource);
  45. if ($errorNumber > 0) {
  46. throw new Exception('Curl error: #' . $errorNumber . ' - ' . $errorMessage);
  47. }
  48. $response = $request->client->createResponse($responseContent, $responseHeaders);
  49. $request->afterSend($response);
  50. return $response;
  51. }
  52. /**
  53. * @inheritdoc
  54. */
  55. public function batchSend(array $requests)
  56. {
  57. $curlBatchResource = curl_multi_init();
  58. $token = '';
  59. $curlResources = [];
  60. $responseHeaders = [];
  61. foreach ($requests as $key => $request) {
  62. /* @var $request Request */
  63. $request->beforeSend();
  64. $curlOptions = $this->prepare($request);
  65. $curlResource = $this->initCurl($curlOptions);
  66. $token .= $request->client->createRequestLogToken($request->getMethod(), $curlOptions[CURLOPT_URL], $curlOptions[CURLOPT_HTTPHEADER], $request->getContent()) . "\n\n";
  67. $responseHeaders[$key] = [];
  68. $this->setHeaderOutput($curlResource, $responseHeaders[$key]);
  69. $curlResources[$key] = $curlResource;
  70. curl_multi_add_handle($curlBatchResource, $curlResource);
  71. }
  72. Yii::info($token, __METHOD__);
  73. Yii::beginProfile($token, __METHOD__);
  74. try {
  75. $isRunning = null;
  76. do {
  77. // See https://bugs.php.net/bug.php?id=61141
  78. if (curl_multi_select($curlBatchResource) === -1) {
  79. usleep(100);
  80. }
  81. do {
  82. $curlExecCode = curl_multi_exec($curlBatchResource, $isRunning);
  83. } while ($curlExecCode === CURLM_CALL_MULTI_PERFORM);
  84. } while ($isRunning > 0 && $curlExecCode === CURLM_OK);
  85. } catch (\Exception $e) {
  86. Yii::endProfile($token, __METHOD__);
  87. throw new Exception($e->getMessage(), $e->getCode(), $e);
  88. }
  89. Yii::endProfile($token, __METHOD__);
  90. $responseContents = [];
  91. foreach ($curlResources as $key => $curlResource) {
  92. $responseContents[$key] = curl_multi_getcontent($curlResource);
  93. curl_multi_remove_handle($curlBatchResource, $curlResource);
  94. }
  95. curl_multi_close($curlBatchResource);
  96. $responses = [];
  97. foreach ($requests as $key => $request) {
  98. $response = $request->client->createResponse($responseContents[$key], $responseHeaders[$key]);
  99. $request->afterSend($response);
  100. $responses[$key] = $response;
  101. }
  102. return $responses;
  103. }
  104. /**
  105. * Prepare request for execution, creating cURL resource for it.
  106. * @param Request $request request instance.
  107. * @return array cURL options.
  108. */
  109. private function prepare($request)
  110. {
  111. $request->prepare();
  112. $curlOptions = $this->composeCurlOptions($request->getOptions());
  113. $method = strtoupper($request->getMethod());
  114. switch ($method) {
  115. case 'POST':
  116. $curlOptions[CURLOPT_POST] = true;
  117. break;
  118. default:
  119. $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
  120. }
  121. $content = $request->getContent();
  122. if ($content === null) {
  123. if ($method === 'HEAD') {
  124. $curlOptions[CURLOPT_NOBODY] = true;
  125. }
  126. } else {
  127. $curlOptions[CURLOPT_POSTFIELDS] = $content;
  128. }
  129. $curlOptions[CURLOPT_RETURNTRANSFER] = true;
  130. $curlOptions[CURLOPT_URL] = $request->getFullUrl();
  131. $curlOptions[CURLOPT_HTTPHEADER] = $request->composeHeaderLines();
  132. return $curlOptions;
  133. }
  134. /**
  135. * Initializes cURL resource.
  136. * @param array $curlOptions cURL options.
  137. * @return resource prepared cURL resource.
  138. */
  139. private function initCurl(array $curlOptions)
  140. {
  141. $curlResource = curl_init();
  142. foreach ($curlOptions as $option => $value) {
  143. curl_setopt($curlResource, $option, $value);
  144. }
  145. return $curlResource;
  146. }
  147. /**
  148. * Composes cURL options from raw request options.
  149. * @param array $options raw request options.
  150. * @return array cURL options, in format: [curl_constant => value].
  151. */
  152. private function composeCurlOptions(array $options)
  153. {
  154. static $optionMap = [
  155. 'protocolVersion' => CURLOPT_HTTP_VERSION,
  156. 'maxRedirects' => CURLOPT_MAXREDIRS,
  157. 'sslCapath' => CURLOPT_CAPATH,
  158. 'sslCafile' => CURLOPT_CAINFO,
  159. ];
  160. $curlOptions = [];
  161. foreach ($options as $key => $value) {
  162. if (is_int($key)) {
  163. $curlOptions[$key] = $value;
  164. } else {
  165. if (isset($optionMap[$key])) {
  166. $curlOptions[$optionMap[$key]] = $value;
  167. } else {
  168. $key = strtoupper($key);
  169. if (strpos($key, 'SSL') === 0) {
  170. $key = substr($key, 3);
  171. $constantName = 'CURLOPT_SSL_' . $key;
  172. if (!defined($constantName)) {
  173. $constantName = 'CURLOPT_SSL' . $key;
  174. }
  175. } else {
  176. $constantName = 'CURLOPT_' . strtoupper($key);
  177. }
  178. $curlOptions[constant($constantName)] = $value;
  179. }
  180. }
  181. }
  182. return $curlOptions;
  183. }
  184. /**
  185. * Setup a variable, which should collect the cURL response headers.
  186. * @param resource $curlResource cURL resource.
  187. * @param array $output variable, which should collection headers.
  188. */
  189. private function setHeaderOutput($curlResource, array &$output)
  190. {
  191. curl_setopt($curlResource, CURLOPT_HEADERFUNCTION, function($resource, $headerString) use (&$output) {
  192. $header = trim($headerString, "\n\r");
  193. if (strlen($header) > 0) {
  194. $output[] = $header;
  195. }
  196. return mb_strlen($headerString, '8bit');
  197. });
  198. }
  199. }