Completed
Push — master ( 988ea9...868c80 )
by smiley
07:26
created

CurlMultiClient::process()   B

Complexity

Conditions 11
Paths 14

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 14
nop 0
dl 0
loc 47
rs 7.3166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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_getinfo, 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_array, is_resource, usleep;
23
24
use const CURLM_OK, CURLMOPT_MAXCONNECTS, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX;
25
26
class CurlMultiClient implements LoggerAwareInterface{
27
	use LoggerAwareTrait;
28
29
	/** @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\HTTP\HTTPOptions */
30
	protected SettingsContainerInterface $options;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_STRING, expecting T_FUNCTION or T_CONST
Loading history...
31
32
	protected ResponseFactoryInterface $responseFactory;
33
34
	protected ?MultiResponseHandlerInterface $multiResponseHandler = null;
35
36
	/**
37
	 * the curl_multi master handle
38
	 *
39
	 * @var resource
40
	 */
41
	protected $curl_multi;
42
43
	/**
44
	 * An array of RequestInterface to run
45
	 *
46
	 * @var \Psr\Http\Message\RequestInterface[]
47
	 */
48
	protected array $requests = [];
49
50
	/**
51
	 * the stack of running handles
52
	 *
53
	 * @var \chillerlan\HTTP\CurlUtils\CurlHandle[]
54
	 */
55
	protected array $handles = [];
56
57
	/**
58
	 * @var int
59
	 */
60
	protected int $handleCounter = 0;
61
62
	/**
63
	 * CurlMultiClient constructor.
64
	 *
65
	 * @param \chillerlan\Settings\SettingsContainerInterface|null $options
66
	 * @param \Psr\Http\Message\ResponseFactoryInterface|null      $responseFactory
67
	 * @param \Psr\Log\LoggerInterface|null                        $logger
68
	 */
69
	public function __construct(
70
		SettingsContainerInterface $options = null,
71
		ResponseFactoryInterface $responseFactory = null,
72
		LoggerInterface $logger = null
73
	){
74
		$this->options         = $options ?? new HTTPOptions;
75
		$this->responseFactory = $responseFactory ?? new ResponseFactory;
76
		$this->logger          = $logger ?? new NullLogger;
77
		$this->curl_multi      = curl_multi_init();
78
79
		$curl_multi_options = [
80
			CURLMOPT_PIPELINING  => CURLPIPE_MULTIPLEX,
81
			CURLMOPT_MAXCONNECTS => $this->options->windowSize,
82
		] + $this->options->curl_multi_options;
83
84
		foreach($curl_multi_options as $k => $v){
85
			curl_multi_setopt($this->curl_multi, $k, $v);
86
		}
87
88
	}
89
90
	/**
91
	 * close an existing cURL multi handle on exit
92
	 */
93
	public function __destruct(){
94
		$this->close();
95
	}
96
97
	/**
98
	 * @return void
99
	 */
100
	public function close():void{
101
102
		if(is_resource($this->curl_multi)){
103
			curl_multi_close($this->curl_multi);
104
		}
105
106
	}
107
108
	/**
109
	 * @param \chillerlan\HTTP\CurlUtils\MultiResponseHandlerInterface $handler
110
	 *
111
	 * @return \chillerlan\HTTP\CurlUtils\CurlMultiClient
112
	 */
113
	public function setMultiResponseHandler(MultiResponseHandlerInterface $handler):CurlMultiClient{
114
		$this->multiResponseHandler = $handler;
115
116
		return $this;
117
	}
118
119
	/**
120
	 * @param \Psr\Http\Message\RequestInterface $request
121
	 *
122
	 * @return \chillerlan\HTTP\CurlUtils\CurlMultiClient
123
	 */
124
	public function addRequest(RequestInterface $request):CurlMultiClient{
125
		$this->requests[] = $request;
126
127
		return $this;
128
	}
129
130
	/**
131
	 * @param \Psr\Http\Message\RequestInterface[] $stack
132
	 *
133
	 * @return \chillerlan\HTTP\CurlUtils\CurlMultiClient
134
	 */
135
	public function addRequests(iterable $stack):CurlMultiClient{
136
137
		foreach($stack as $request){
138
139
			if($request instanceof RequestInterface){
140
				$this->requests[] = $request;
141
			}
142
143
		}
144
145
		return $this;
146
	}
147
148
	/**
149
	 * @throws \chillerlan\HTTP\Psr18\ClientException
150
	 */
151
	public function process():CurlMultiClient{
152
153
		if(empty($this->requests)){
154
			throw new ClientException('request stack is empty');
155
		}
156
157
		if(!$this->multiResponseHandler instanceof MultiResponseHandlerInterface){
158
			throw new ClientException('no response handler set');
159
		}
160
161
		// shoot out the first batch of requests
162
		for($i = 0; $i < $this->options->windowSize; $i++){
163
			$this->createHandle();
164
		}
165
166
		// ...and process the stack
167
		do{
168
			$status = curl_multi_exec($this->curl_multi, $active);
169
170
			if($active){
171
				curl_multi_select($this->curl_multi, $this->options->timeout);
172
			}
173
174
			while($state = curl_multi_info_read($this->curl_multi)){
175
				$id     = (int)$state['handle'];
176
				$handle = $this->handles[$id];
177
				$info   = curl_getinfo($handle->curl);
178
				$result = $this->multiResponseHandler->handleResponse($handle->response, $handle->request, $handle->id, (is_array($info) ? $info : []));
179
180
				curl_multi_remove_handle($this->curl_multi, $state['handle']);
181
				curl_close($state['handle']);
182
				unset($this->handles[$id]);
183
184
				if($result instanceof RequestInterface && $handle->retries < $this->options->retries){
185
					$this->createHandle($result, $handle->id, ++$handle->retries);
186
187
					continue;
188
				}
189
190
				$this->createHandle();
191
			}
192
193
		}
194
		while($active && $status === CURLM_OK);
195
196
		return $this;
197
	}
198
199
	/**
200
	 *
201
	 */
202
	protected function createHandle(RequestInterface $request = null, int $id = null, int $retries = null):void{
203
204
		if($request === null){
205
206
			if(empty($this->requests)){
207
				return;
208
			}
209
210
			$request = array_shift($this->requests);
211
		}
212
213
		$handle          = new $this->options->curlHandle($request, $this->responseFactory->createResponse(), $this->options);
214
		$handle->id      = $id ?? $this->handleCounter++;
215
		$handle->retries = $retries ?? 1;
216
217
		$handle->init();
218
		curl_multi_add_handle($this->curl_multi, $handle->curl);
219
220
		$this->handles[(int)$handle->curl] = $handle;
221
222
		if($this->options->sleep > 0){
223
			usleep($this->options->sleep);
224
		}
225
226
	}
227
228
}
229