Passed
Push — gh-pages ( 22b0fe...eb2d91 )
by
unknown
12:27 queued 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
 * @created      30.08.2018
6
 * @author       smiley <[email protected]>
7
 * @copyright    2018 smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\HTTP\CurlUtils;
12
13
use chillerlan\HTTP\{HTTPOptions, Psr17\ResponseFactory, Psr18\ClientException};
14
use chillerlan\Settings\SettingsContainerInterface;
15
use Psr\Http\Message\{RequestInterface, ResponseFactoryInterface};
16
use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger};
17
18
use function array_shift, curl_close, curl_multi_add_handle, curl_multi_close, curl_multi_exec,
19
	curl_multi_info_read, curl_multi_init, curl_multi_remove_handle, curl_multi_select, curl_multi_setopt,
20
	is_resource, usleep;
21
22
use const CURLM_OK, CURLMOPT_MAXCONNECTS, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX;
23
24
final class CurlMultiClient implements LoggerAwareInterface{
25
	use LoggerAwareTrait;
26
27
	/** @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\HTTP\HTTPOptions */
28
	private SettingsContainerInterface $options;
29
30
	private ResponseFactoryInterface $responseFactory;
31
32
	private MultiResponseHandlerInterface $multiResponseHandler;
33
34
	/**
35
	 * the curl_multi master handle
36
	 *
37
	 * @var resource
38
	 */
39
	private $curl_multi;
40
41
	/**
42
	 * An array of RequestInterface to run
43
	 *
44
	 * @var \Psr\Http\Message\RequestInterface[]
45
	 */
46
	private array $requests = [];
47
48
	/**
49
	 * the stack of running handles
50
	 *
51
	 * @var \chillerlan\HTTP\CurlUtils\CurlHandle[]
52
	 */
53
	private array $handles = [];
54
55
	/**
56
	 *
57
	 */
58
	private int $handleCounter = 0;
59
60
	/**
61
	 * CurlMultiClient constructor.
62
	 */
63
	public function __construct(
64
		MultiResponseHandlerInterface $multiResponseHandler,
65
		SettingsContainerInterface $options = null,
66
		ResponseFactoryInterface $responseFactory = null,
67
		LoggerInterface $logger = null
68
	){
69
		$this->multiResponseHandler = $multiResponseHandler;
70
		$this->options              = $options ?? new HTTPOptions;
71
		$this->responseFactory      = $responseFactory ?? new ResponseFactory;
72
		$this->logger               = $logger ?? new NullLogger;
73
		$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...
74
75
		$curl_multi_options = [
76
			CURLMOPT_PIPELINING  => CURLPIPE_MULTIPLEX,
77
			CURLMOPT_MAXCONNECTS => $this->options->windowSize,
78
		] + $this->options->curl_multi_options;
79
80
		foreach($curl_multi_options as $k => $v){
81
			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

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