CurlMultiClient::close()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 2
b 0
f 0
nc 2
nop 0
dl 0
loc 4
rs 10
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
use CurlMultiHandle as CMH;
18
19
use function array_shift, curl_close, curl_multi_add_handle, curl_multi_close, curl_multi_exec,
20
	curl_multi_info_read, curl_multi_init, curl_multi_remove_handle, curl_multi_select, curl_multi_setopt, usleep;
21
22
use const CURLM_OK, CURLMOPT_MAXCONNECTS, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX;
23
24
class CurlMultiClient implements LoggerAwareInterface{
25
	use LoggerAwareTrait;
26
27
	protected HTTPOptions|SettingsContainerInterface $options;
28
	protected ResponseFactoryInterface               $responseFactory;
29
	protected MultiResponseHandlerInterface          $multiResponseHandler;
30
	protected ?CMH                                   $curl_multi           = null;
31
	protected int                                    $handleCounter        = 0;
32
33
	/**
34
	 * An array of RequestInterface to run
35
	 */
36
	protected array $requests = [];
37
38
	/**
39
	 * the stack of running handles
40
	 *
41
	 * @var \chillerlan\HTTP\CurlUtils\CurlMultiHandle[]
42
	 */
43
	protected array $handles = [];
44
45
	/**
46
	 * CurlMultiClient constructor.
47
	 */
48
	public function __construct(
49
		MultiResponseHandlerInterface $multiResponseHandler,
50
		HTTPOptions|SettingsContainerInterface $options = null,
51
		ResponseFactoryInterface $responseFactory = null,
52
		LoggerInterface $logger = null
53
	){
54
		$this->multiResponseHandler = $multiResponseHandler;
55
		$this->options              = $options ?? new HTTPOptions;
56
		$this->responseFactory      = $responseFactory ?? new ResponseFactory;
57
		$this->logger               = $logger ?? new NullLogger;
58
		$this->curl_multi           = curl_multi_init();
59
60
		$curl_multi_options = [
61
			CURLMOPT_PIPELINING  => CURLPIPE_MULTIPLEX,
62
			CURLMOPT_MAXCONNECTS => $this->options->window_size,
0 ignored issues
show
Bug Best Practice introduced by
The property $window_size is declared protected in chillerlan\HTTP\HTTPOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
63
		] + $this->options->curl_multi_options;
0 ignored issues
show
Bug Best Practice introduced by
The property $curl_multi_options is declared protected in chillerlan\HTTP\HTTPOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
64
65
		foreach($curl_multi_options as $k => $v){
66
			curl_multi_setopt($this->curl_multi, $k, $v);
67
		}
68
69
	}
70
71
	/**
72
	 * close an existing cURL multi handle on exit
73
	 */
74
	public function __destruct(){
75
		$this->close();
76
	}
77
78
	/**
79
	 *
80
	 */
81
	public function close():void{
82
83
		if($this->curl_multi instanceof CMH){
84
			curl_multi_close($this->curl_multi);
85
		}
86
87
	}
88
89
	/**
90
	 *
91
	 */
92
	public function addRequest(RequestInterface $request):CurlMultiClient{
93
		$this->requests[] = $request;
94
95
		return $this;
96
	}
97
98
	/**
99
	 * @param \Psr\Http\Message\RequestInterface[] $stack
100
	 */
101
	public function addRequests(iterable $stack):CurlMultiClient{
102
103
		foreach($stack as $request){
104
105
			if($request instanceof RequestInterface){
106
				$this->requests[] = $request;
107
			}
108
109
		}
110
111
		return $this;
112
	}
113
114
	/**
115
	 * @phan-suppress PhanTypeInvalidThrowsIsInterface
116
	 * @throws \Psr\Http\Client\ClientExceptionInterface
117
	 */
118
	public function process():CurlMultiClient{
119
120
		if(empty($this->requests)){
121
			throw new ClientException('request stack is empty');
122
		}
123
124
		// shoot out the first batch of requests
125
		for($i = 0; $i < $this->options->window_size; $i++){
0 ignored issues
show
Bug Best Practice introduced by
The property $window_size is declared protected in chillerlan\HTTP\HTTPOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
126
			$this->createHandle();
127
		}
128
129
		// ...and process the stack
130
		do{
131
			$status = curl_multi_exec($this->curl_multi, $active);
132
133
			if($active){
134
				curl_multi_select($this->curl_multi, $this->options->timeout);
0 ignored issues
show
Bug Best Practice introduced by
The property $timeout is declared protected in chillerlan\HTTP\HTTPOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
135
			}
136
137
			while($state = curl_multi_info_read($this->curl_multi)){
138
				$id     = (int)$state['handle'];
139
				$handle = $this->handles[$id];
140
				$result = $handle->handleResponse();
141
142
				curl_multi_remove_handle($this->curl_multi, $state['handle']);
143
				curl_close($state['handle']);
144
				unset($this->handles[$id]);
145
146
				if($result instanceof RequestInterface && $handle->getRetries() < $this->options->retries){
0 ignored issues
show
Bug Best Practice introduced by
The property $retries is declared protected in chillerlan\HTTP\HTTPOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
147
					$this->createHandle($result, $handle->getID(), $handle->addRetry());
148
149
					continue;
150
				}
151
152
				$this->createHandle();
153
			}
154
155
		}
156
		while($active && $status === CURLM_OK);
157
158
		return $this;
159
	}
160
161
	/**
162
	 *
163
	 */
164
	protected function createHandle(RequestInterface $request = null, int $id = null, int $retries = null):void{
165
166
		if($request === null){
167
168
			if(empty($this->requests)){
169
				return;
170
			}
171
172
			$request = array_shift($this->requests);
173
		}
174
175
		$handle = new CurlMultiHandle(
176
			$this->multiResponseHandler,
177
			$request,
178
			$this->responseFactory->createResponse(),
179
			$this->options
180
		);
181
182
		$curl = $handle
183
			->setID($id ?? $this->handleCounter++)
184
			->setRetries($retries ?? 1)
185
			->init()
186
		;
187
188
		/** @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal */
189
		curl_multi_add_handle($this->curl_multi, $curl);
190
191
		$this->handles[(int)$curl] = $handle;
192
193
		if($this->options->sleep > 0){
0 ignored issues
show
Bug Best Practice introduced by
The property $sleep is declared protected in chillerlan\HTTP\HTTPOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
194
			usleep($this->options->sleep);
0 ignored issues
show
Bug introduced by
It seems like $this->options->sleep can also be of type null; however, parameter $microseconds of usleep() does only seem to accept integer, 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

194
			usleep(/** @scrutinizer ignore-type */ $this->options->sleep);
Loading history...
195
		}
196
197
	}
198
199
}
200