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