Passed
Branch master (e903e1)
by
unknown
02:26
created

MultiRequest::createHandle()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 11
nc 4
nop 0
1
<?php
2
/**
3
 * Class MultiRequest
4
 *
5
 * @filesource   MultiRequest.php
6
 * @created      15.02.2016
7
 * @package      chillerlan\TinyCurl
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2016 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\TinyCurl;
14
15
/**
16
 * @link http://www.onlineaspect.com/2009/01/26/how-to-use-curl_multi-without-blocking/
17
 * @link https://github.com/joshfraser/rolling-curl
18
 *
19
 * (there are countless implementations around, just google for "php rolling curl")
20
 */
21
class MultiRequest{
22
23
	/**
24
	 * the curl_multi master handle
25
	 *
26
	 * @var resource
27
	 */
28
	protected $curl_multi;
29
30
	/**
31
	 * cURL options for each handle
32
	 *
33
	 * @var array
34
	 */
35
	protected $curl_options = [];
36
37
	/**
38
	 * An array of the request URLs
39
	 *
40
	 * @var array<\chillerlan\TinyCurl\URL>
41
	 */
42
	protected $stack = [];
43
44
	/**
45
	 * The returned value from MultiResponseHandlerInterface::handleResponse() for each request
46
	 *
47
	 * @var array
48
	 */
49
	protected $responses = [];
50
51
	/**
52
	 * @var \chillerlan\TinyCurl\MultiRequestOptions
53
	 */
54
	protected $options;
55
56
	/**
57
	 * @var \chillerlan\TinyCurl\MultiResponseHandlerInterface
58
	 */
59
	protected $multiResponseHandler;
60
61
	/**
62
	 * MultiRequest constructor.
63
	 *
64
	 * @param \chillerlan\TinyCurl\MultiRequestOptions $options
0 ignored issues
show
Documentation introduced by
Should the type for parameter $options not be null|MultiRequestOptions?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
65
	 */
66
	public function __construct(MultiRequestOptions $options = null){
67
		$this->setOptions($options ?: new MultiRequestOptions);
68
	}
69
70
	/**
71
	 * closes the curl_multi instance
72
	 *
73
	 * @codeCoverageIgnore
74
	 */
75
	public function __destruct(){
76
		if($this->curl_multi){
77
			curl_multi_close($this->curl_multi);
78
		}
79
	}
80
81
	/**
82
	 * @param \chillerlan\TinyCurl\MultiRequestOptions $options
83
	 *
84
	 * @return \chillerlan\TinyCurl\MultiRequest
85
	 * @throws \chillerlan\TinyCurl\RequestException
86
	 */
87
	public function setOptions(MultiRequestOptions $options):MultiRequest {
88
		$this->options = $options;
89
90
		if($this->options->handler){
91
			$this->setHandler();
92
		}
93
94
		$this->curl_options = $this->options->curl_options + [
95
			CURLOPT_HEADER         => true,
96
			CURLOPT_RETURNTRANSFER => true,
97
			CURLOPT_USERAGENT      => $this->options->user_agent,
98
			CURLOPT_PROTOCOLS      => CURLPROTO_HTTP|CURLPROTO_HTTPS,
99
			CURLOPT_SSL_VERIFYPEER => true,
100
			CURLOPT_SSL_VERIFYHOST => 2, // Support for value 1 removed in cURL 7.28.1
101
			CURLOPT_CAINFO         => is_file($this->options->ca_info) ? $this->options->ca_info : null,
102
		];
103
104
		return $this;
105
	}
106
107
	/**
108
	 * @param \chillerlan\TinyCurl\MultiResponseHandlerInterface $handler
0 ignored issues
show
Documentation introduced by
Should the type for parameter $handler not be null|MultiResponseHandlerInterface?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
109
	 *
110
	 * @return \chillerlan\TinyCurl\MultiRequest
111
	 * @throws \chillerlan\TinyCurl\RequestException
112
	 */
113
	public function setHandler(MultiResponseHandlerInterface $handler = null):MultiRequest {
114
115
		if(!$handler){
116
117
			if(!class_exists($this->options->handler)){
118
				throw new RequestException('no handler set');
119
			}
120
121
			$handler = new $this->options->handler($this);
122
123
			if(!is_a($handler, MultiResponseHandlerInterface::class)){
124
				throw new RequestException('handler is not a MultiResponseHandlerInterface');
125
			}
126
127
		}
128
129
		$this->multiResponseHandler = $handler;
130
131
		return $this;
132
	}
133
134
	/**
135
	 * @param array $urls array of \chillerlan\TinyCurl\URL objects
136
	 *
137
	 * @return void
138
	 * @throws \chillerlan\TinyCurl\RequestException
139
	 */
140
	public function fetch(array $urls){
141
142
		if(empty($urls)){
143
			throw new RequestException('$urls is empty');
144
		}
145
146
		$this->stack      = $urls;
147
		$this->curl_multi = curl_multi_init();
148
149
		curl_multi_setopt($this->curl_multi, CURLMOPT_PIPELINING, 2);
150
		curl_multi_setopt($this->curl_multi, CURLMOPT_MAXCONNECTS, $this->options->window_size);
151
152
		// shoot out the first batch of requests
153
		array_map(function(){
154
			$this->createHandle();
155
		}, range(1, $this->options->window_size));
156
157
		/// ...and start processing the stack
158
		$this->processStack();
159
	}
160
161
	/**
162
	 * @see \chillerlan\TinyCurl\MultiResponseHandlerInterface
163
	 *
164
	 * @param mixed $response
165
	 *
166
	 * @return void
167
	 */
168
	public function addResponse($response){
169
		$this->responses[] = $response;
170
	}
171
172
	/**
173
	 * @return array
174
	 */
175
	public function getResponseData():array {
176
		return $this->responses;
177
	}
178
179
	/**
180
	 * creates a new cURL handle
181
	 *
182
	 * @return void
183
	 */
184
	protected function createHandle(){
185
186
		if(!empty($this->stack)){
187
			$url = array_shift($this->stack);
188
189
			if($url instanceof URL){
190
				$curl = curl_init($url->mergeParams());
191
192
				curl_setopt_array($curl, $this->curl_options);
193
				curl_multi_add_handle($this->curl_multi, $curl);
194
195
				if($this->options->sleep){
196
					usleep($this->options->sleep);
197
				}
198
199
			}
200
			else{
201
				// retry on next if we don't get what we expect
202
				$this->createHandle(); // @codeCoverageIgnore
203
			}
204
205
		}
206
207
	}
208
209
	/**
210
	 * processes the requests
211
	 *
212
	 * @return void
213
	 */
214
	protected function processStack(){
215
216
		do{
217
218
			do {
219
				$status = curl_multi_exec($this->curl_multi, $active);
220
			}
221
			while($status === CURLM_CALL_MULTI_PERFORM);
222
223
			// welcome to callback hell.
224
			while($state = curl_multi_info_read($this->curl_multi)){
225
				$url = $this->multiResponseHandler->handleResponse(new MultiResponse($state['handle']));
226
227
				if($url instanceof URL){
228
					$this->stack[] = $url;
229
				}
230
231
				curl_multi_remove_handle($this->curl_multi, $state['handle']);
232
				curl_close($state['handle']);
233
				$this->createHandle();
234
			}
235
236
			if($active){
237
				curl_multi_select($this->curl_multi, $this->options->timeout);
238
			}
239
240
		}
241
		while($active && $status === CURLM_OK);
242
243
	}
244
245
}
246