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