1<?php 2namespace GuzzleHttp\Ring\Client; 3 4use GuzzleHttp\Ring\Future\CompletedFutureArray; 5use GuzzleHttp\Ring\Core; 6 7/** 8 * HTTP handler that uses cURL easy handles as a transport layer. 9 * 10 * Requires PHP 5.5+ 11 * 12 * When using the CurlHandler, custom curl options can be specified as an 13 * associative array of curl option constants mapping to values in the 14 * **curl** key of the "client" key of the request. 15 */ 16class CurlHandler 17{ 18 /** @var callable */ 19 private $factory; 20 21 /** @var array Array of curl easy handles */ 22 private $handles = []; 23 24 /** @var array Array of owned curl easy handles */ 25 private $ownedHandles = []; 26 27 /** @var int Total number of idle handles to keep in cache */ 28 private $maxHandles; 29 30 /** 31 * Accepts an associative array of options: 32 * 33 * - factory: Optional callable factory used to create cURL handles. 34 * The callable is passed a request hash when invoked, and returns an 35 * array of the curl handle, headers resource, and body resource. 36 * - max_handles: Maximum number of idle handles (defaults to 5). 37 * 38 * @param array $options Array of options to use with the handler 39 */ 40 public function __construct(array $options = []) 41 { 42 $this->handles = $this->ownedHandles = []; 43 $this->factory = isset($options['handle_factory']) 44 ? $options['handle_factory'] 45 : new CurlFactory(); 46 $this->maxHandles = isset($options['max_handles']) 47 ? $options['max_handles'] 48 : 5; 49 } 50 51 public function __destruct() 52 { 53 foreach ($this->handles as $handle) { 54 if (is_resource($handle)) { 55 curl_close($handle); 56 } 57 } 58 } 59 60 /** 61 * @param array $request 62 * 63 * @return CompletedFutureArray 64 */ 65 public function __invoke(array $request) 66 { 67 return new CompletedFutureArray( 68 $this->_invokeAsArray($request) 69 ); 70 } 71 72 /** 73 * @internal 74 * 75 * @param array $request 76 * 77 * @return array 78 */ 79 public function _invokeAsArray(array $request) 80 { 81 $factory = $this->factory; 82 83 // Ensure headers are by reference. They're updated elsewhere. 84 $result = $factory($request, $this->checkoutEasyHandle()); 85 $h = $result[0]; 86 $hd =& $result[1]; 87 $bd = $result[2]; 88 Core::doSleep($request); 89 curl_exec($h); 90 $response = ['transfer_stats' => curl_getinfo($h)]; 91 $response['curl']['error'] = curl_error($h); 92 $response['curl']['errno'] = curl_errno($h); 93 $response['transfer_stats'] = array_merge($response['transfer_stats'], $response['curl']); 94 $this->releaseEasyHandle($h); 95 96 return CurlFactory::createResponse([$this, '_invokeAsArray'], $request, $response, $hd, $bd); 97 } 98 99 private function checkoutEasyHandle() 100 { 101 // Find an unused handle in the cache 102 if (false !== ($key = array_search(false, $this->ownedHandles, true))) { 103 $this->ownedHandles[$key] = true; 104 return $this->handles[$key]; 105 } 106 107 // Add a new handle 108 $handle = curl_init(); 109 $id = (int) $handle; 110 $this->handles[$id] = $handle; 111 $this->ownedHandles[$id] = true; 112 113 return $handle; 114 } 115 116 private function releaseEasyHandle($handle) 117 { 118 $id = (int) $handle; 119 if (count($this->ownedHandles) > $this->maxHandles) { 120 curl_close($this->handles[$id]); 121 unset($this->handles[$id], $this->ownedHandles[$id]); 122 } else { 123 // curl_reset doesn't clear these out for some reason 124 static $unsetValues = [ 125 CURLOPT_HEADERFUNCTION => null, 126 CURLOPT_WRITEFUNCTION => null, 127 CURLOPT_READFUNCTION => null, 128 CURLOPT_PROGRESSFUNCTION => null, 129 ]; 130 curl_setopt_array($handle, $unsetValues); 131 curl_reset($handle); 132 $this->ownedHandles[$id] = false; 133 } 134 } 135} 136