Completed
Push — master ( 21fcc7...da5913 )
by smiley
02:45
created

MultiRequest::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 0 Features 1
Metric Value
c 7
b 0
f 1
dl 0
loc 9
rs 9.6666
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
/**
3
 *
4
 * @filesource   MultiRequest.php
5
 * @created      15.02.2016
6
 * @package      chillerlan\TinyCurl
7
 * @author       Smiley <[email protected]>
8
 * @copyright    2016 Smiley
9
 * @license      MIT
10
 */
11
12
namespace chillerlan\TinyCurl;
13
14
use chillerlan\TinyCurl\Response\MultiResponse;
15
use chillerlan\TinyCurl\Response\MultiResponseHandlerInterface;
16
17
/**
18
 * Class MultiRequest
19
 *
20
 * @link http://www.onlineaspect.com/2009/01/26/how-to-use-curl_multi-without-blocking/
21
 */
22
class MultiRequest{
23
24
	/**
25
	 * the curl_multi master handle
26
	 *
27
	 * @var resource
28
	 */
29
	protected $curl_multi;
30
31
	/**
32
	 * cURL options for each handle
33
	 *
34
	 * @var array
35
	 */
36
	protected $curl_options = [];
37
38
	/**
39
	 * An array of the request URLs
40
	 *
41
	 * @var array<\chillerlan\TinyCurl\URL>
42
	 */
43
	protected $stack = [];
44
45
	/**
46
	 * The returned value from MultiResponseHandlerInterface::handleResponse() for each request
47
	 *
48
	 * @var array
49
	 */
50
	protected $responses = [];
51
52
	/**
53
	 * @var \chillerlan\TinyCurl\MultiRequestOptions
54
	 */
55
	protected $options;
56
57
	/**
58
	 * @var \chillerlan\TinyCurl\Response\MultiResponseHandlerInterface
59
	 */
60
	protected $multiResponseHandler;
61
62
	/**
63
	 * MultiRequest constructor.
64
	 *
65
	 * @param \chillerlan\TinyCurl\MultiRequestOptions|null $options
66
	 */
67
	public function __construct(MultiRequestOptions $options = null){
68
69
		if(!$options){
70
			$options = new MultiRequestOptions;
71
		}
72
73
		$this->options = $options;
74
		$this->setOptions();
75
	}
76
77
	/**
78
	 * closes the curl_multi instance
79
	 *
80
	 * @codeCoverageIgnore
81
	 */
82
	public function __destruct(){
83
		if($this->curl_multi){
84
			curl_multi_close($this->curl_multi);
85
		}
86
	}
87
88
	/**
89
	 * @param \chillerlan\TinyCurl\Response\MultiResponseHandlerInterface|null $handler
90
	 *
91
	 * @return $this
92
	 * @throws \chillerlan\TinyCurl\RequestException
93
	 */
94
	public function setHandler(MultiResponseHandlerInterface $handler = null){
95
96
		if(!$handler){
97
98
			if(!class_exists($this->options->handler)){
99
				throw new RequestException('!$this->options->handler');
100
			}
101
102
			$handler = new $this->options->handler($this);
103
104
			if(!is_a($handler, MultiResponseHandlerInterface::class)){
105
				throw new RequestException('!is_a($handler)');
106
			}
107
108
		}
109
110
		$this->multiResponseHandler = $handler;
111
112
		return $this;
113
	}
114
115
	/**
116
	 * @param array $urls array of \chillerlan\TinyCurl\URL objects
117
	 *
118
	 * @return $this
119
	 * @throws \chillerlan\TinyCurl\RequestException
120
	 */
121
	public function fetch(array $urls){
122
123
		if(empty($urls)){
124
			throw new RequestException('empty($urls)');
125
		}
126
127
		$this->stack      = $urls;
128
		$this->curl_multi = curl_multi_init();
129
130
		curl_multi_setopt($this->curl_multi, CURLMOPT_PIPELINING, 2);
131
		curl_multi_setopt($this->curl_multi, CURLMOPT_MAXCONNECTS, $this->options->window_size);
132
133
		// shoot out the first batch of requests
134
		array_map(function(){
135
			$this->createHandle();
136
			usleep(100);
137
		}, range(1, $this->options->window_size));
138
139
		/// ...and start processing the stack
140
		$this->processStack();
141
142
		return $this;
143
	}
144
145
	/**
146
	 * @param mixed $response
147
	 *
148
	 * @see \chillerlan\TinyCurl\Response\MultiResponseHandlerInterface
149
	 * @return $this
150
	 */
151
	public function addResponse($response){
152
		$this->responses[] = $response;
153
154
		return $this;
155
	}
156
157
	/**
158
	 * @return array
159
	 */
160
	public function getResponseData(){
161
		return $this->responses;
162
	}
163
164
	/**
165
	 * @return void
166
	 * @throws \chillerlan\TinyCurl\RequestException
167
	 */
168
	protected function setOptions(){
169
170
		if($this->options->handler){
171
			$this->setHandler();
172
		}
173
174
		$ca_info = is_file($this->options->ca_info) ? $this->options->ca_info : null;
175
		$this->curl_options = $this->options->curl_options + [
176
				CURLOPT_RETURNTRANSFER => true,
177
				CURLOPT_SSL_VERIFYPEER => (bool)$ca_info,
178
				CURLOPT_SSL_VERIFYHOST => 2, // Support for value 1 removed in cURL 7.28.1
179
				CURLOPT_CAINFO         => $ca_info,
180
				CURLOPT_HEADER         => true,
181
			];
182
	}
183
184
	/**
185
	 * creates a new cURL handle
186
	 */
187
	protected function createHandle(){
188
189
		if(!empty($this->stack)){
190
			$url = array_shift($this->stack);
191
192
			if($url instanceof URL){
193
				$curl = curl_init($url);
194
				curl_setopt_array($curl, $this->curl_options);
195
				curl_multi_add_handle($this->curl_multi, $curl);
196
			}
197
			else{
198
				// retry on next if we don't get what we expect
199
				$this->createHandle();
200
			}
201
202
		}
203
204
	}
205
206
	/**
207
	 * processes the requests
208
	 */
209
	protected function processStack(){
210
211
		do{
212
213
			if(curl_multi_exec($this->curl_multi, $active) !== CURLM_OK){
214
				break; // @codeCoverageIgnore
215
			}
216
217
			// welcome to callback hell.
218
			while($state = curl_multi_info_read($this->curl_multi)){
219
				$url = $this->multiResponseHandler->handleResponse(new MultiResponse($state['handle']));
220
221
				if($url instanceof URL){
222
					$this->stack[] = $url;
223
				}
224
225
				$this->createHandle();
226
				curl_multi_remove_handle($this->curl_multi, $state['handle']);
227
			}
228
229
			if($active){
230
				curl_multi_select($this->curl_multi, $this->options->timeout);
231
			}
232
233
		}
234
		while($active);
235
236
	}
237
238
}
239