• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

voku / httpful / 5623107679

pending completion
5623107679

push

github

voku
[+]: fix test for the new release

1596 of 2486 relevant lines covered (64.2%)

81.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.89
/src/Httpful/Curl/MultiCurl.php
1
<?php declare(strict_types=1);
2

3
namespace Httpful\Curl;
4

5
/**
6
 * @internal
7
 */
8
final class MultiCurl
9
{
10
    /**
11
     * @var resource|\CurlMultiHandle
12
     */
13
    private $multiCurl;
14

15
    /**
16
     * @var Curl[]
17
     */
18
    private $curls = [];
19

20
    /**
21
     * @var Curl[]
22
     */
23
    private $activeCurls = [];
24

25
    /**
26
     * @var bool
27
     */
28
    private $isStarted = false;
29

30
    /**
31
     * @var int
32
     */
33
    private $concurrency = 25;
34

35
    /**
36
     * @var int
37
     */
38
    private $nextCurlId = 0;
39

40
    /**
41
     * @var callable|null
42
     */
43
    private $beforeSendCallback;
44

45
    /**
46
     * @var callable|null
47
     */
48
    private $successCallback;
49

50
    /**
51
     * @var callable|null
52
     */
53
    private $errorCallback;
54

55
    /**
56
     * @var callable|null
57
     */
58
    private $completeCallback;
59

60
    /**
61
     * @var callable|int
62
     */
63
    private $retry;
64

65
    /**
66
     * @var array
67
     */
68
    private $cookies = [];
69

70
    public function __construct()
71
    {
72
        $this->multiCurl = \curl_multi_init();
24✔
73
    }
74

75
    public function __destruct()
76
    {
77
        $this->close();
24✔
78
    }
79

80
    /**
81
     * Add a Curl instance to the handle queue.
82
     *
83
     * @param Curl $curl
84
     *
85
     * @return $this
86
     */
87
    public function addCurl(Curl $curl)
88
    {
89
        $this->queueHandle($curl);
24✔
90

91
        return $this;
24✔
92
    }
93

94
    /**
95
     * @param callable $callback
96
     *
97
     * @return $this
98
     */
99
    public function beforeSend($callback)
100
    {
101
        $this->beforeSendCallback = $callback;
×
102

103
        return $this;
×
104
    }
105

106
    /**
107
     * @return void
108
     */
109
    public function close()
110
    {
111
        foreach ($this->curls as $curl) {
24✔
112
            $curl->close();
×
113
        }
114

115
        if (
116
            \is_resource($this->multiCurl)
24✔
117
            ||
118
            (class_exists('CurlMultiHandle') && $this->multiCurl instanceof \CurlMultiHandle)
24✔
119
        ) {
120
            \curl_multi_close($this->multiCurl);
24✔
121
        }
122
    }
123

124
    /**
125
     * @param callable $callback
126
     *
127
     * @return $this;
128
     */
129
    public function complete($callback)
130
    {
131
        $this->completeCallback = $callback;
12✔
132

133
        return $this;
12✔
134
    }
135

136
    /**
137
     * @param callable $callback
138
     *
139
     * @return $this
140
     */
141
    public function error($callback)
142
    {
143
        $this->errorCallback = $callback;
×
144

145
        return $this;
×
146
    }
147

148
    /**
149
     * @param Curl            $curl
150
     * @param callable|string $mixed_filename
151
     *
152
     * @return Curl
153
     */
154
    public function addDownload(Curl $curl, $mixed_filename)
155
    {
156
        $this->queueHandle($curl);
×
157

158
        // Use tmpfile() or php://temp to avoid "Too many open files" error.
159
        if (\is_callable($mixed_filename)) {
×
160
            $curl->downloadCompleteCallback = $mixed_filename;
×
161
            $curl->downloadFileName = null;
×
162
            $curl->fileHandle = \tmpfile();
×
163
        } else {
164
            $filename = $mixed_filename;
×
165

166
            // Use a temporary file when downloading. Not using a temporary file can cause an error when an existing
167
            // file has already fully completed downloading and a new download is started with the same destination save
168
            // path. The download request will include header "Range: bytes=$filesize-" which is syntactically valid,
169
            // but unsatisfiable.
170
            $download_filename = $filename . '.pccdownload';
×
171
            $curl->downloadFileName = $download_filename;
×
172

173
            // Attempt to resume download only when a temporary download file exists and is not empty.
174
            if (\is_file($download_filename) && $filesize = \filesize($download_filename)) {
×
175
                $first_byte_position = $filesize;
×
176
                $range = $first_byte_position . '-';
×
177
                $curl->setRange($range);
×
178
                $curl->fileHandle = \fopen($download_filename, 'ab');
×
179

180
                // Move the downloaded temporary file to the destination save path.
181
                $curl->downloadCompleteCallback = static function ($instance, $fh) use ($download_filename, $filename) {
×
182
                    // Close the open file handle before renaming the file.
183
                    if (\is_resource($fh)) {
×
184
                        \fclose($fh);
×
185
                    }
186

187
                    \rename($download_filename, $filename);
×
188
                };
×
189
            } else {
190
                $curl->fileHandle = \fopen('php://temp', 'wb');
×
191
                $curl->downloadCompleteCallback = static function ($instance, $fh) use ($filename) {
×
192
                    \file_put_contents($filename, \stream_get_contents($fh));
×
193
                };
×
194
            }
195
        }
196

197
        if ($curl->fileHandle === false) {
×
198
            throw new \Httpful\Exception\ClientErrorException('Unable to write to file:' . $curl->downloadFileName);
×
199
        }
200

201
        $curl->setFile($curl->fileHandle);
×
202
        $curl->setOpt(\CURLOPT_CUSTOMREQUEST, 'GET');
×
203
        $curl->setOpt(\CURLOPT_HTTPGET, true);
×
204

205
        return $curl;
×
206
    }
207

208
    /**
209
     * @param int $concurrency
210
     *
211
     * @return $this
212
     */
213
    public function setConcurrency($concurrency)
214
    {
215
        $this->concurrency = $concurrency;
×
216

217
        return $this;
×
218
    }
219

220
    /**
221
     * @param string $key
222
     * @param mixed  $value
223
     *
224
     * @return $this
225
     */
226
    public function setCookie($key, $value)
227
    {
228
        $this->cookies[$key] = $value;
×
229

230
        return $this;
×
231
    }
232

233
    /**
234
     * @param array $cookies
235
     *
236
     * @return $this
237
     */
238
    public function setCookies($cookies)
239
    {
240
        foreach ($cookies as $key => $value) {
×
241
            $this->cookies[$key] = $value;
×
242
        }
243

244
        return $this;
×
245
    }
246

247
    /**
248
     * Number of retries to attempt or decider callable.
249
     *
250
     * When using a number of retries to attempt, the maximum number of attempts
251
     * for the request is $maximum_number_of_retries + 1.
252
     *
253
     * When using a callable decider, the request will be retried until the
254
     * function returns a value which evaluates to false.
255
     *
256
     * @param callable|int $mixed
257
     *
258
     * @return $this
259
     */
260
    public function setRetry($mixed)
261
    {
262
        $this->retry = $mixed;
×
263

264
        return $this;
×
265
    }
266

267
    /**
268
     * @return $this|null
269
     */
270
    public function start()
271
    {
272
        if ($this->isStarted) {
24✔
273
            return null;
×
274
        }
275

276
        $this->isStarted = true;
24✔
277

278
        $concurrency = $this->concurrency;
24✔
279
        if ($concurrency > \count($this->curls)) {
24✔
280
            $concurrency = \count($this->curls);
24✔
281
        }
282

283
        for ($i = 0; $i < $concurrency; ++$i) {
24✔
284
            $curlOrNull = \array_shift($this->curls);
24✔
285
            if ($curlOrNull !== null) {
24✔
286
                $this->initHandle($curlOrNull);
24✔
287
            }
288
        }
289

290
        $active = null;
24✔
291
        do {
292
            // Wait for activity on any curl_multi connection when curl_multi_select (libcurl) fails to correctly block.
293
            // https://bugs.php.net/bug.php?id=63411
294
            if ($active && \curl_multi_select($this->multiCurl) === -1) {
24✔
295
                \usleep(250);
×
296
            }
297

298
            \curl_multi_exec($this->multiCurl, $active);
24✔
299

300
            while (!(($info_array = \curl_multi_info_read($this->multiCurl)) === false)) {
24✔
301
                if ($info_array['msg'] === \CURLMSG_DONE) {
24✔
302
                    foreach ($this->activeCurls as $key => $curl) {
24✔
303
                        $curlRes = $curl->getCurl();
24✔
304
                        if ($curlRes === false) {
24✔
305
                            continue;
×
306
                        }
307

308
                        if ($curlRes === $info_array['handle']) {
24✔
309
                            // Set the error code for multi handles using the "result" key in the array returned by
310
                            // curl_multi_info_read(). Using curl_errno() on a multi handle will incorrectly return 0
311
                            // for errors.
312
                            $curl->curlErrorCode = $info_array['result'];
24✔
313
                            $curl->exec($curlRes);
24✔
314

315
                            if ($curl->attemptRetry()) {
24✔
316
                                // Remove completed handle before adding again in order to retry request.
317
                                \curl_multi_remove_handle($this->multiCurl, $curlRes);
×
318

319
                                $curlm_error_code = \curl_multi_add_handle($this->multiCurl, $curlRes);
×
320
                                if ($curlm_error_code !== \CURLM_OK) {
×
321
                                    throw new \ErrorException(
×
322
                                        'cURL multi add handle error: ' . \curl_multi_strerror($curlm_error_code)
×
323
                                    );
×
324
                                }
325
                            } else {
326
                                $curl->execDone();
24✔
327

328
                                // Remove completed instance from active curls.
329
                                unset($this->activeCurls[$key]);
24✔
330

331
                                // Start new requests before removing the handle of the completed one.
332
                                while (\count($this->curls) >= 1 && \count($this->activeCurls) < $this->concurrency) {
24✔
333
                                    $curlOrNull = \array_shift($this->curls);
×
334
                                    if ($curlOrNull !== null) {
×
335
                                        $this->initHandle($curlOrNull);
×
336
                                    }
337
                                }
338
                                \curl_multi_remove_handle($this->multiCurl, $curlRes);
24✔
339

340
                                // Clean up completed instance.
341
                                $curl->close();
24✔
342
                            }
343

344
                            break;
24✔
345
                        }
346
                    }
347
                }
348
            }
349

350
            if (!$active) {
24✔
351
                $active = \count($this->activeCurls);
24✔
352
            }
353
        } while ($active > 0);
24✔
354

355
        $this->isStarted = false;
24✔
356

357
        return $this;
24✔
358
    }
359

360
    /**
361
     * @param callable $callback
362
     *
363
     * @return $this
364
     */
365
    public function success($callback)
366
    {
367
        $this->successCallback = $callback;
12✔
368

369
        return $this;
12✔
370
    }
371

372
    /**
373
     * @return resource|\CurlMultiHandle
374
     */
375
    public function getMultiCurl()
376
    {
377
        return $this->multiCurl;
×
378
    }
379

380
    /**
381
     * @param Curl $curl
382
     *
383
     * @throws \ErrorException
384
     *
385
     * @return void
386
     */
387
    private function initHandle($curl)
388
    {
389
        // Set callbacks if not already individually set.
390

391
        if ($curl->beforeSendCallback === null) {
24✔
392
            $curl->beforeSend($this->beforeSendCallback);
24✔
393
        }
394

395
        if ($curl->successCallback === null) {
24✔
396
            $curl->success($this->successCallback);
24✔
397
        }
398

399
        if ($curl->errorCallback === null) {
24✔
400
            $curl->error($this->errorCallback);
24✔
401
        }
402

403
        if ($curl->completeCallback === null) {
24✔
404
            $curl->complete($this->completeCallback);
24✔
405
        }
406

407
        $curl->setRetry($this->retry);
24✔
408
        $curl->setCookies($this->cookies);
24✔
409

410
        $curlRes = $curl->getCurl();
24✔
411
        if ($curlRes === false) {
24✔
412
            throw new \ErrorException('cURL multi add handle error from curl: curl === false');
×
413
        }
414

415
        $curlm_error_code = \curl_multi_add_handle($this->multiCurl, $curlRes);
24✔
416
        if ($curlm_error_code !== \CURLM_OK) {
24✔
417
            throw new \ErrorException('cURL multi add handle error: ' . \curl_multi_strerror($curlm_error_code));
×
418
        }
419

420
        $this->activeCurls[$curl->getId()] = $curl;
24✔
421
        $curl->call($curl->beforeSendCallback);
24✔
422
    }
423

424
    /**
425
     * @param Curl $curl
426
     *
427
     * @return void
428
     */
429
    private function queueHandle($curl)
430
    {
431
        // Use sequential ids to allow for ordered post processing.
432
        ++$this->nextCurlId;
24✔
433
        $curl->setId($this->nextCurlId);
24✔
434
        $curl->setChildOfMultiCurl(true);
24✔
435
        $this->curls[$this->nextCurlId] = $curl;
24✔
436
    }
437
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc