Passed
Push — main ( f2efe3...53dc0a )
by smiley
10:15
created

CurlMultiClient::process()   B

Complexity

Conditions 9
Paths 13

Size

Total Lines 41
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 21
nc 13
nop 0
dl 0
loc 41
rs 8.0555
c 1
b 0
f 0
1
<?php
2
/**
3
 * Class CurlMultiClient
4
 *
5
 * @filesource   CurlMultiClient.php
6
 * @created      30.08.2018
7
 * @package      chillerlan\HTTP\CurlUtils
8
 * @author       smiley <[email protected]>
9
 * @copyright    2018 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\HTTP\CurlUtils;
14
15
use chillerlan\HTTP\{HTTPOptions, Psr17\ResponseFactory, Psr18\ClientException};
16
use chillerlan\Settings\SettingsContainerInterface;
17
use Psr\Http\Message\{RequestInterface, ResponseFactoryInterface};
18
use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger};
19
20
use function array_shift, curl_close, curl_multi_add_handle, curl_multi_close, curl_multi_exec,
21
	curl_multi_info_read, curl_multi_init, curl_multi_remove_handle, curl_multi_select, curl_multi_setopt,
22
	is_resource, usleep;
23
24
use const CURLM_OK, CURLMOPT_MAXCONNECTS, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX;
25
26
final class CurlMultiClient implements LoggerAwareInterface{
27
	use LoggerAwareTrait;
28
29
	/** @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\HTTP\HTTPOptions */
30
	private SettingsContainerInterface $options;
31
32
	private ResponseFactoryInterface $responseFactory;
33
34
	private MultiResponseHandlerInterface $multiResponseHandler;
35
36
	/**
37
	 * the curl_multi master handle
38
	 *
39
	 * @var resource
40
	 */
41
	private $curl_multi;
42
43
	/**
44
	 * An array of RequestInterface to run
45
	 *
46
	 * @var \Psr\Http\Message\RequestInterface[]
47
	 */
48
	private array $requests = [];
49
50
	/**
51
	 * the stack of running handles
52
	 *
53
	 * @var \chillerlan\HTTP\CurlUtils\CurlHandle[]
54
	 */
55
	private array $handles = [];
56
57
	/**
58
	 *
59
	 */
60
	private int $handleCounter = 0;
61
62
	/**
63
	 * CurlMultiClient constructor.
64
	 */
65
	public function __construct(
66
		MultiResponseHandlerInterface $multiResponseHandler,
67
		SettingsContainerInterface $options = null,
68
		ResponseFactoryInterface $responseFactory = null,
69
		LoggerInterface $logger = null
70
	){
71
		$this->multiResponseHandler = $multiResponseHandler;
72
		$this->options              = $options ?? new HTTPOptions;
73
		$this->responseFactory      = $responseFactory ?? new ResponseFactory;
74
		$this->logger               = $logger ?? new NullLogger;
75
		$this->curl_multi           = curl_multi_init();
0 ignored issues
show
Documentation Bug introduced by
It seems like curl_multi_init() of type CurlMultiHandle or true is incompatible with the declared type resource of property $curl_multi.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
76
77
		$curl_multi_options = [
78
			CURLMOPT_PIPELINING  => CURLPIPE_MULTIPLEX,
79
			CURLMOPT_MAXCONNECTS => $this->options->windowSize,
80
		] + $this->options->curl_multi_options;
81
82
		foreach($curl_multi_options as $k => $v){
83
			curl_multi_setopt($this->curl_multi, $k, $v);
0 ignored issues
show
Bug introduced by
It seems like $this->curl_multi can also be of type true; however, parameter $multi_handle of curl_multi_setopt() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

83
			curl_multi_setopt(/** @scrutinizer ignore-type */ $this->curl_multi, $k, $v);
Loading history...
84
		}
85
86
	}
87
88
	/**
89
	 * close an existing cURL multi handle on exit
90
	 */
91
	public function __destruct(){
92
		$this->close();
93
	}
94
95
	/**
96
	 * @return void
97
	 */
98
	public function close():void{
99
100
		if(is_resource($this->curl_multi)){
101
			curl_multi_close($this->curl_multi);
102
		}
103
104
	}
105
106
	/**
107
	 * @param \Psr\Http\Message\RequestInterface $request
108
	 *
109
	 * @return \chillerlan\HTTP\CurlUtils\CurlMultiClient
110
	 */
111
	public function addRequest(RequestInterface $request):CurlMultiClient{
112
		$this->requests[] = $request;
113
114
		return $this;
115
	}
116
117
	/**
118
	 * @param \Psr\Http\Message\RequestInterface[] $stack
119
	 *
120
	 * @return \chillerlan\HTTP\CurlUtils\CurlMultiClient
121
	 */
122
	public function addRequests(iterable $stack):CurlMultiClient{
123
124
		foreach($stack as $request){
125
126
			if($request instanceof RequestInterface){
127
				$this->requests[] = $request;
128
			}
129
130
		}
131
132
		return $this;
133
	}
134
135
	/**
136
	 * @phan-suppress PhanTypeInvalidThrowsIsInterface
137
	 * @throws \Psr\Http\Client\ClientExceptionInterface
138
	 */
139
	public function process():CurlMultiClient{
140
141
		if(empty($this->requests)){
142
			throw new ClientException('request stack is empty');
143
		}
144
145
		// shoot out the first batch of requests
146
		for($i = 0; $i < $this->options->windowSize; $i++){
147
			$this->createHandle();
148
		}
149
150
		// ...and process the stack
151
		do{
152
			$status = curl_multi_exec($this->curl_multi, $active);
153
154
			if($active){
155
				curl_multi_select($this->curl_multi, $this->options->timeout);
156
			}
157
158
			while($state = curl_multi_info_read($this->curl_multi)){
159
				$id     = (int)$state['handle'];
160
				$handle = $this->handles[$id];
161
				$result = $handle->handleResponse();
162
163
				curl_multi_remove_handle($this->curl_multi, $state['handle']);
164
				curl_close($state['handle']);
165
				unset($this->handles[$id]);
166
167
				if($result instanceof RequestInterface && $handle->getRetries() < $this->options->retries){
168
					$this->createHandle($result, $handle->getID(), $handle->addRetry());
169
170
					continue;
171
				}
172
173
				$this->createHandle();
174
			}
175
176
		}
177
		while($active && $status === CURLM_OK);
178
179
		return $this;
180
	}
181
182
	/**
183
	 *
184
	 */
185
	private function createHandle(RequestInterface $request = null, int $id = null, int $retries = null):void{
186
187
		if($request === null){
188
189
			if(empty($this->requests)){
190
				return;
191
			}
192
193
			$request = array_shift($this->requests);
194
		}
195
196
		$handle = new CurlMultiHandle(
197
			$this->multiResponseHandler,
198
			$request,
199
			$this->responseFactory->createResponse(),
200
			$this->options
201
		);
202
203
		$handle
204
			->setID($id ?? $this->handleCounter++)
205
			->setRetries($retries ?? 1)
206
			->init()
207
		;
208
209
		$curl = $handle->getCurlResource();
210
211
		curl_multi_add_handle($this->curl_multi, $curl);
212
213
		$this->handles[(int)$curl] = $handle;
214
215
		if($this->options->sleep > 0){
216
			usleep($this->options->sleep);
217
		}
218
219
	}
220
221
}
222