Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.48% covered (success)
90.48%
95 / 105
76.19% covered (success)
76.19%
16 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
CurlMulti
90.48% covered (success)
90.48%
95 / 105
76.19% covered (success)
76.19%
16 / 21
71.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOption
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addClient
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 addClients
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClients
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeClient
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 getClientContent
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 parseResponse
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getAllResponses
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
5.01
 getInfo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setWait
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isComplete
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 isSuccess
71.43% covered (success)
71.43%
10 / 14
0.00% covered (danger)
0.00%
0 / 1
15.36
 isError
84.62% covered (success)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
10.36
 send
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sendAsync
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 disconnect
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Http\Client\Handler;
15
16use Pop\Http\Auth;
17use Pop\Http\Client;
18use Pop\Http\Client\Request;
19use Pop\Http\Client\Response;
20use Pop\Http\Promise;
21
22/**
23 * HTTP client curl multi handler class
24 *
25 * @category   Pop
26 * @package    Pop\Http
27 * @author     Nick Sagona, III <dev@nolainteractive.com>
28 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
29 * @license    http://www.popphp.org/license     New BSD License
30 * @version    5.0.0
31 */
32class CurlMulti extends AbstractCurl
33{
34
35    /**
36     * Curl clients
37     * @var array
38     */
39    protected array $clients = [];
40
41    /**
42     * Constructor
43     *
44     * Instantiate the Curl multi handler object
45     *
46     * @param  ?array $options
47     * @throws Exception
48     */
49    public function __construct(?array $options = null)
50    {
51        if (!function_exists('curl_multi_init')) {
52            throw new Exception('Error: Curl multi handler support is not available.');
53        }
54
55        $this->resource = curl_multi_init();
56
57        if (!empty($options)) {
58            $this->setOptions($options);
59        }
60    }
61
62    /**
63     * Factory method to create a Curl multi handler
64     *
65     * @param  ?array $options
66     * @return CurlMulti
67     */
68    public static function create(?array $options = null): CurlMulti
69    {
70        return new self($options);
71    }
72
73    /**
74     * Set Curl option
75     *
76     * @param  int   $opt
77     * @param  mixed $val
78     * @return AbstractCurl
79     */
80    public function setOption(int $opt, mixed $val): AbstractCurl
81    {
82        parent::setOption($opt, $val);
83        curl_multi_setopt($this->resource, $opt, $val);
84
85        return $this;
86    }
87
88    /**
89     * Add Curl client
90     *
91     * @param  Client $curlClient
92     * @param  ?string $name
93     * @return CurlMulti
94     */
95    public function addClient(Client $curlClient, ?string $name = null): CurlMulti
96    {
97        if (!in_array($curlClient, $this->clients) && ($curlClient->hasHandler())) {
98            if ($name !== null) {
99                $this->clients[$name] = $curlClient;
100            } else {
101                $this->clients[] = $curlClient;
102            }
103
104            $curlClient->getHandler()->prepare($curlClient->getRequest(), $curlClient->getAuth());
105
106            curl_multi_add_handle($this->resource, $curlClient->getHandler()->resource());
107        }
108
109        return $this;
110    }
111
112    /**
113     * Add Curl clients
114     *
115     * @param  array $clients
116     * @return CurlMulti
117     */
118    public function addClients(array $clients): CurlMulti
119    {
120        foreach ($clients as $name => $client) {
121            if (is_numeric($name)) {
122                $name = null;
123            }
124            $this->addClient($client, $name);
125        }
126
127        return $this;
128    }
129
130    /**
131     * Get Curl client
132     *
133     * @return Client|null
134     */
135    public function getClient(string $name): Client|null
136    {
137        return $this->clients[$name] ?? null;
138    }
139
140    /**
141     * Has Curl client
142     *
143     * @return bool
144     */
145    public function hasClient(string $name): bool
146    {
147        return isset($this->clients[$name]);
148    }
149
150    /**
151     * Get Curl clients
152     *
153     * @return array
154     */
155    public function getClients(): array
156    {
157        return $this->clients;
158    }
159
160    /**
161     * Remove Curl client
162     *
163     * @param  ?string      $name
164     * @param  Client|null $curlClient
165     * @throws Exception
166     * @return CurlMulti
167     */
168    public function removeClient(?string $name = null, Client|null $curlClient = null): CurlMulti
169    {
170        if (($name !== null) && isset($this->clients[$name])) {
171            $curlClient = $this->clients[$name];
172            unset($this->clients[$name]);
173        } else if ($curlClient !== null) {
174            foreach ($this->clients as $i => $client) {
175                if ($client == $curlClient) {
176                    unset($this->clients[$i]);
177                }
178            }
179        } else {
180            throw new Exception('Error: You must pass at least a name or client parameter.');
181        }
182
183        $curlResource = $curlClient?->getHandler()?->resource();
184
185        if (!empty($curlResource)) {
186            curl_multi_remove_handle($this->resource, $curlResource);
187        }
188
189        return $this;
190    }
191
192    /**
193     * Get Curl client content
194     *
195     * @param  string|Client $curlClient
196     * @return Client|string|null
197     */
198    public function getClientContent(string|Client $curlClient): Client|string|null
199    {
200        if (is_string($curlClient) && isset($this->clients[$curlClient])) {
201            if ($this->clients[$curlClient] instanceof Client) {
202                $curlClient  = $this->clients[$curlClient];
203                $curlResource = $curlClient?->getHandler()?->resource();
204            }
205        } else {
206            $curlResource = $curlClient?->getHandler()?->resource();
207        }
208
209        $response = (!empty($curlResource)) ? curl_multi_getcontent($curlResource) : null;
210        if (!empty($response)) {
211            $curlClient->getHandler()->setResponse($response);
212            return $curlClient;
213        } else {
214            return $response;
215        }
216    }
217
218    /**
219     * Get Curl response content
220     *
221     * @param  string|Client $curlClient
222     * @return mixed
223     */
224    public function parseResponse(string|Client $curlClient): mixed
225    {
226        $response = $this->getClientContent($curlClient);
227
228        if ($curlClient instanceof Client) {
229            return $curlClient->getHandler()->parseResponse();
230        } else {
231            return $response;
232        }
233    }
234
235    /**
236     * Get all responses
237     *
238     * @return array
239     */
240    public function getAllResponses(): array
241    {
242        $responses = [];
243
244        foreach ($this->clients as $curlClient) {
245            $response = $this->parseResponse($curlClient);
246            if ($response instanceof Response) {
247                $auto        = (($curlClient->hasOption('auto')) && ($curlClient->getOption('auto')));
248                $responses[] = [
249                    'client_uri' => $curlClient->getRequest()->getUriAsString(),
250                    'method'     => $curlClient->getRequest()->getMethod(),
251                    'code'       => $response->getCode(),
252                    'response'   => ($auto) ? $response->getParsedResponse() : $response
253                ];
254            } else {
255                $responses[] = $response;
256            }
257        }
258
259        return $responses;
260    }
261
262    /**
263     * Get info about the Curl multi-handler
264     *
265     * @return array|false
266     */
267    public function getInfo(): array|false
268    {
269        return curl_multi_info_read($this->resource);
270    }
271
272    /**
273     * Set a wait time until there is any activity on a connection
274     *
275     * @return int
276     */
277    public function setWait(float $timeout = 1.0): int
278    {
279        return curl_multi_select($this->resource, $timeout);
280    }
281
282    /**
283     * Determine if the response is complete
284     *
285     * @return bool
286     */
287    public function isComplete(): bool
288    {
289        $info = $this->getInfo();
290        return (is_array($info) && isset($info['msg']) && ($info['msg'] == CURLMSG_DONE));
291    }
292
293    /**
294     * Determine if the response is a success
295     *
296     * @param  bool $strict
297     * @return bool|null
298     */
299    public function isSuccess(bool $strict = true): bool|null
300    {
301        $result = null;
302
303        if ($this->isComplete()) {
304            $responses = $this->getAllResponses();
305            $result    = true;
306            foreach ($responses as $response) {
307                if (!empty($response['code'])) {
308                    $codeResult = floor($response['code'] / 100);
309                    if (($strict) && (!(($codeResult == 1) || ($codeResult == 2) || ($codeResult == 3)))) {
310                        $result = false;
311                        break;
312                    } else if ((!$strict) && ((($codeResult == 1) || ($codeResult == 2) || ($codeResult == 3)))) {
313                        $result = true;
314                        break;
315                    }
316                }
317            }
318        }
319
320        return $result;
321    }
322
323    /**
324     * Determine if the response is an error
325     *
326     * @param  bool $strict
327     * @return bool|null
328     */
329    public function isError(bool $strict = false): bool|null
330    {
331        $result = null;
332
333        if ($this->isComplete()) {
334            $responses = $this->getAllResponses();
335            foreach ($responses as $response) {
336                if (!empty($response['code'])) {
337                    $codeResult = floor($response['code'] / 100);
338                    if (($strict) && (!(($codeResult == 4) || ($codeResult == 5)))) {
339                        $result = false;
340                        break;
341                    } else if ((!$strict) && ((($codeResult == 4) || ($codeResult == 5)))) {
342                        $result = true;
343                        break;
344                    }
345                }
346            }
347        }
348
349        return $result;
350    }
351
352    /**
353     * Method to send the multiple Curl connections
354     *
355     * @param  ?int $active
356     * @return int
357     */
358    public function send(?int &$active = null): int
359    {
360        return curl_multi_exec($this->resource, $active);
361    }
362
363    /**
364     * Method to send the request asynchronously
365     *
366     * @return Promise
367     */
368    public function sendAsync(): Promise
369    {
370        return new Promise($this);
371    }
372
373    /**
374     * Method to reset the handler
375     *
376     * @return CurlMulti
377     */
378    public function reset(): CurlMulti
379    {
380        foreach ($this->clients as $key => $curlClient) {
381            $this->removeClient($key);
382        }
383
384        $this->clients = [];
385
386        return $this;
387    }
388
389    /**
390     * Close the handler connection
391     *
392     * @return void
393     */
394    public function disconnect(): void
395    {
396        if ($this->hasResource()) {
397            curl_multi_close($this->resource);
398            $this->resource = null;
399            $this->options  = [];
400            $this->clients  = [];
401        }
402    }
403
404}