├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── example └── facebook_sites_info.php ├── src ├── Client.php └── Result.php └── tests ├── CaseTest.php ├── QueryTest.php ├── SimpleTest.php └── data └── travis_nginx.conf /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | composer.phar 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | - 7.0 9 | - hhvm 10 | - nightly 11 | 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - php: 7.0 16 | - php: hhvm 17 | - php: nightly 18 | 19 | env: 20 | - TEST_DOMAIN=localhost 21 | 22 | before_script: 23 | - sudo apt-get install software-properties-common 24 | - sudo add-apt-repository -y ppa:nginx/stable 25 | - sudo apt-get update -qq 26 | - sudo apt-get install -qq nginx 27 | - sudo cp tests/data/travis_nginx.conf /etc/nginx/nginx.conf 28 | - sudo /etc/init.d/nginx restart 29 | - composer install 30 | 31 | script: phpunit tests/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 KhristenkoYura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MCurl - simple, but functional wrapper for curl 2 | ========= 3 | [![Build](https://travis-ci.org/KhristenkoYura/mcurl.svg?branch=master)](https://travis-ci.org/KhristenkoYura/mcurl) 4 | [![Version](https://img.shields.io/packagist/v/khr/php-mcurl-client.svg)](https://packagist.org/packages/khr/php-mcurl-client) 5 | [![License](https://img.shields.io/packagist/l/khr/php-mcurl-client.svg)](https://github.com/KhristenkoYura/mcurl/blob/master/LICENSE) 6 | [![Downloads](https://img.shields.io/packagist/dt/khr/php-mcurl-client.svg)](https://packagist.org/packages/khr/php-mcurl-client) 7 | ### Features: 8 | - PHP >= 5.3 (compatible up to version 7.0 && hhvm) 9 | - **stable**. Using many projects 10 | - **fast** request. Minimal overhead 11 | - run a query in a single line 12 | - parallel request (Multi request). Default enable parallel request 13 | - use async request 14 | - **balancing requests** 15 | - no callable 16 | 17 | ## Install 18 | 19 | The recommended way to install multi curl is through [composer](http://getcomposer.org). 20 | 21 | $ composer require khr/php-mcurl-client:3.* 22 | ```json 23 | { 24 | "require": { 25 | "khr/php-mcurl-client": "~3.0" 26 | } 27 | } 28 | ``` 29 | 30 | Quick Start and Examples 31 | ======= 32 | 33 | ### Create 34 | ```php 35 | use MCurl\Client; 36 | $client = new Client(); 37 | ``` 38 | ### Simple request 39 | ```php 40 | echo $client->get('http://example.com'); 41 | ``` 42 | ### Check error 43 | ```php 44 | $result = $client->get('http://example.com'); 45 | echo (!$result->hasError() 46 | ? 'Ok: ' . $result 47 | : 'Error: ' .$result->error . ' ('.$result->errorCode.')') 48 | , PHP_EOL; 49 | ``` 50 | ### Add curl options in request 51 | ```php 52 | echo $client->get('http://example.com', [CURLOPT_REFERER => 'http://example.net/']); 53 | ``` 54 | ### Post request 55 | ```php 56 | echo $client->post('http://example.com', ['post-key' => 'post-value'], [CURLOPT_REFERER => 'http://example.net/']); 57 | ``` 58 | ### Simple parallel request 59 | ```php 60 | // @var $results Result[] 61 | $results = $client->get(['http://example.com', 'http://example.net']); 62 | foreach($results as $result) { 63 | echo $result; 64 | } 65 | ``` 66 | ### Parallel request 67 | ```php 68 | $urls = ['http://example.com', 'http://example.net', 'http://example.org']; 69 | foreach($urls as $url) { 70 | $client->add([CURLOPT_URL => $url]); 71 | } 72 | // wait all request 73 | // @var $results Result[] 74 | $results = $client->all(); 75 | ``` 76 | ### Parallel request; waiting only next result 77 | ```php 78 | $urls = ['http://example.com', 'http://example.net', 'http://example.org']; 79 | foreach($urls as $url) { 80 | $client->add([CURLOPT_URL => $url]); 81 | } 82 | while($result = $client->next()) { 83 | echo $result; 84 | } 85 | ``` 86 | ### Dynamic add request 87 | ```php 88 | while($result = $client->next()) { 89 | $urls = fun_get_urls_for_parse_result($result); 90 | foreach($urls as $url) { 91 | $client->add([CURLOPT_URL => $url]); 92 | } 93 | echo $result; 94 | } 95 | ``` 96 | ### Non-blocking request; use async code; only run request and check done 97 | ```php 98 | while($client->run() || $client->has()) { 99 | while($client->has()) { 100 | // no blocking 101 | $result = $client->next(); 102 | echo $result; 103 | } 104 | 105 | // more async code 106 | 107 | //end more async code 108 | } 109 | ``` 110 | ### Use params 111 | ```php 112 | $result = $client->add([CURLOPT_URL => $url], ['id' => 7])->next(); 113 | echo $result->params['id']; // echo 7 114 | 115 | ``` 116 | ### Result 117 | ```php 118 | // @var $result Result 119 | $result->body; // string: body result 120 | $result->json; // object; @see json_encode 121 | $result->getJson(true); // array; @see json_encode 122 | $result->headers['content-type']; // use $client->enableHeaders(); 123 | $result->info; // @see curl_getinfo(); 124 | $result->info['total_time']; // 0.001 125 | 126 | $result->hasError(); // not empty curl_error or http code >=400 127 | $result->hasError('network'); // only not empty curl_error 128 | $result->hasError('http'); // only http code >=400 129 | $result->getError(); // return message error, if ->hasError(); 130 | $result->httpCode; // return 200 131 | ``` 132 | ### Config 133 | 134 | #### Client::setOptions 135 | This curl options add in all request 136 | ```php 137 | $client->setOptions([CURLOPT_REFERER => 'http://example.net/']); 138 | ``` 139 | #### Client::enableHeaders 140 | Add headers in result 141 | ```php 142 | $client->enableHeaders(); 143 | ``` 144 | 145 | #### Client::setMaxRequest 146 | The maximum number of queries executed in parallel 147 | ```php 148 | $client->setMaxRequest(20); // set 20 parallel request 149 | ``` 150 | #### Client::setSleep 151 | To balance the requests in the time interval using the method **$client->setSleep**. It will help you to avoid stress (on the sending server) for receiving dynamic content by adjusting the conversion rate in the interval. 152 | Example: 153 | ```php 154 | $client->setSleep (20, 1); 155 | ``` 156 | 1 second will run no more than 20 queries. 157 | 158 | For static content is recommended restrictions on download speeds, that would not score channel. 159 | Example: 160 | ```php 161 | //channel 10 Mb. 162 | $client->setMaxRequest (123); 163 | $client->setOptions([CURLOPT_MAX_RECV_SPEED_LARGE => (10 * 1024 ^ 3) / 123]); 164 | ``` 165 | ### Cookbook 166 | 167 | #### Download file 168 | ```php 169 | $client->get('http://exmaple.com/image.jpg', [CURLOPT_FILE => fopen('/tmp/image.jpg', 'w')]); 170 | ``` 171 | #### Save memory 172 | To reduce memory usage, you can write the query result in a temporary file. 173 | ```php 174 | $client->setStreamResult(Client::STREAM_FILE); // All Result write in tmp file. 175 | ``` 176 | 177 | ```php 178 | /** 179 | * @see tests/ and source 180 | */ 181 | ``` 182 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "khr/php-mcurl-client", 3 | "description": "wrap curl client (http client) for PHP 5.3; using php multi curl, parallel request and write asynchronous code", 4 | "keywords": ["php", "curl","multi", "client", "http", "async", "parallel", "requests", "spider"], 5 | "homepage": "http://github.com/khristenkoyura/mcurl", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Khristenko Yura", 11 | "email": "khristenkoyura@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.0", 16 | "ext-curl": "*" 17 | }, 18 | "autoload": { 19 | "psr-4": {"MCurl\\": "src/"} 20 | }, 21 | "config":{ 22 | "preferred-install": "dist" 23 | }, 24 | "archive":{ 25 | "exclude":["*", ".*", "!/src/*"] 26 | } 27 | } -------------------------------------------------------------------------------- /example/facebook_sites_info.php: -------------------------------------------------------------------------------- 1 | stdClass Object 11 | ( 12 | [id] => http://yandex.ru 13 | [shares] => 19196 14 | [comments] => 7 15 | [time] => 0.284802 16 | ) 17 | 18 | [1] => stdClass Object 19 | ( 20 | [id] => http://mail.ru 21 | [shares] => 510 22 | [comments] => 13 23 | [time] => 0.496161 24 | ) 25 | 26 | [2] => stdClass Object 27 | ( 28 | [id] => http://facebook.com 29 | [shares] => 14174562 30 | [comments] => 1331 31 | [time] => 0.526038 32 | ) 33 | 34 | [3] => stdClass Object 35 | ( 36 | [id] => http://google.com 37 | [shares] => 9623503 38 | [comments] => 10117 39 | [time] => 0.575178 40 | ) 41 | ) 42 | execution time: 0.587344 43 | execution time sum: 1.882179 44 | all comments: 11468 45 | */ 46 | 47 | include __DIR__ . '/../vendor/autoload.php'; 48 | 49 | use MCurl\Client; 50 | 51 | $sites = array( 52 | 'google.com', 53 | 'yandex.ru', 54 | 'facebook.com', 55 | 'mail.ru', 56 | ); 57 | $results = array(); 58 | $all_comments = 0; 59 | 60 | $client = new Client; 61 | 62 | foreach($sites as $site) { 63 | $client->add(array( 64 | CURLOPT_URL => 'https://graph.facebook.com/http://' . $site 65 | )); 66 | } 67 | 68 | $time = microtime(true); 69 | $all_time = 0; 70 | while($result = $client->next()) { 71 | $info = $result->json; 72 | $all_time+= $info->time = $result->info['total_time']; 73 | 74 | if (!empty($info->comments)) { 75 | $all_comments+=$info->comments; 76 | } 77 | 78 | $results[] = $info; 79 | } 80 | 81 | $time-=microtime(true); 82 | $time*=-1; 83 | 84 | 85 | 86 | echo 'results: ', PHP_EOL; 87 | print_r($results); 88 | echo 'execution time: ', sprintf('%.6f',$time), PHP_EOL; 89 | echo 'execution time sum: ', sprintf('%.6f',$all_time), PHP_EOL; 90 | echo 'all comments: ', $all_comments, PHP_EOL; -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | addCurlOption(), ->getCurlOption(), ->delCurlOption() 44 | * @var array 45 | */ 46 | protected $curlOptions = array( 47 | CURLOPT_BINARYTRANSFER => true, 48 | CURLOPT_RETURNTRANSFER => true, 49 | CURLOPT_TIMEOUT => 60, 50 | ); 51 | 52 | /** 53 | * Max asynchron request 54 | * @var int 55 | */ 56 | protected $maxRequest = 10; 57 | 58 | /** 59 | * Return result class 60 | * @var int microsecond 61 | */ 62 | protected $classResult = '\\MCurl\\Result'; 63 | 64 | /** 65 | * Sleep script undo $this->sleepNext request 66 | * @var int microsecond 67 | */ 68 | protected $sleep = 0; 69 | 70 | /** 71 | * @see $this->sleep 72 | * @var int 73 | */ 74 | protected $sleepNext; 75 | 76 | protected $sleepBlocking; 77 | 78 | protected $sleepNextTime; 79 | 80 | /** 81 | * Count executed request 82 | * @var int 83 | */ 84 | protected $count = 0; 85 | 86 | /** 87 | * Save results 88 | * @var array 89 | */ 90 | protected $results = array(); 91 | 92 | /** 93 | * @see curl_multi_init() 94 | * @var null 95 | */ 96 | protected $mh; 97 | 98 | /** 99 | * @var curl_share_init 100 | */ 101 | protected $sh; 102 | 103 | /** 104 | * has Request 105 | * @var bool 106 | */ 107 | protected $isRunMh = false; 108 | 109 | /** 110 | * Has use blocking function curl_multi_select 111 | * @var bool 112 | */ 113 | protected $isSelect = true; 114 | 115 | /** 116 | * @example self::STREAM_MEMORY 117 | * @see http://php.net/manual/ru/wrappers.php 118 | * @var string 119 | */ 120 | protected $streamResult = null; 121 | 122 | protected $enableHeaders = false; 123 | 124 | /** 125 | * @see http://php.net/manual/ru/stream.filters.php 126 | * @var array 127 | */ 128 | protected $streamFilters = array(); 129 | 130 | /** 131 | * @var string 132 | */ 133 | protected $baseUrl; 134 | 135 | public function __construct() { 136 | $this->mh = curl_multi_init(); 137 | } 138 | 139 | /** 140 | * This parallel request if maxRequest > 1 141 | * $client->get('http://example.com/1.php') => return Result 142 | * Or $client->get('http://example.com/1.php', 'http://example.com/2.php') => return Result[] 143 | * Or $client->get(['http://example.com/1.php', 'http://example.com/2.php'], [CURLOPT_MAX_RECV_SPEED_LARGE => 1024]) => return Result[] 144 | * 145 | * @param string|array $url 146 | * @param array $opts @see http://www.php.net/manual/ru/function.curl-setopt.php 147 | * @return Result|Result[]|null 148 | */ 149 | public function get($url, $opts = array()) { 150 | $urls = (array) $url; 151 | foreach ($urls AS $id => $u) { 152 | $opts[CURLOPT_URL] = $u; 153 | $this->add($opts, $id); 154 | } 155 | return is_array($url) ? $this->all() : $this->next(); 156 | } 157 | 158 | /** 159 | * @see $this->get 160 | * @param $url 161 | * @param array $data post data 162 | * @param array $opts 163 | * @return Result|Result[]|null 164 | */ 165 | public function post($url, $data = array(), $opts = array()) { 166 | $opts[CURLOPT_POST] = true; 167 | $opts[CURLOPT_POSTFIELDS] = $data; 168 | return $this->get($url,$opts); 169 | } 170 | 171 | /** 172 | * Add request 173 | * @param array $opts Options curl. Example: array( CURLOPT_URL => 'http://example.com' ); 174 | * @param array|string $params All data, require binding to the request or if string: identity request 175 | * @return bool 176 | */ 177 | public function add($opts = array(), $params = array()) { 178 | $id = null; 179 | 180 | if (is_string($params)) { 181 | $id = $params; 182 | $params = array(); 183 | } 184 | 185 | if (isset($this->baseUrl, $opts[CURLOPT_URL])) { 186 | $opts[CURLOPT_URL] = $this->baseUrl . $opts[CURLOPT_URL]; 187 | } 188 | 189 | if (isset($this->streamResult) && !isset($opts[CURLOPT_FILE])) { 190 | $opts[CURLOPT_FILE] = fopen($this->streamResult, 'r+'); 191 | if ( !$opts[CURLOPT_FILE] ) { 192 | return false; 193 | } 194 | } 195 | 196 | if (!empty($this->streamFilters ) && isset($opts[CURLOPT_FILE])) { 197 | foreach ($this->streamFilters AS $filter) { 198 | stream_filter_append( $opts[CURLOPT_FILE], $filter ); 199 | } 200 | } 201 | 202 | if (!isset($opts[CURLOPT_WRITEHEADER]) && $this->enableHeaders) { 203 | $opts[CURLOPT_WRITEHEADER] = fopen(self::STREAM_MEMORY, 'r+'); 204 | if (!$opts[CURLOPT_WRITEHEADER]) { 205 | return false; 206 | } 207 | } 208 | 209 | $query = array( 210 | 'id' => $id, 211 | 'opts' => $opts, 212 | 'params' => $params 213 | ); 214 | 215 | 216 | $this->queries[] = $query; 217 | $this->queriesCount++; 218 | 219 | return true; 220 | } 221 | 222 | /** 223 | * Set wrappers 224 | * @see self::STREAM_* 225 | * Default: self::STREAM_MEMORY 226 | * @see http://php.net/manual/ru/wrappers.php 227 | * @param string $stream 228 | */ 229 | public function setStreamResult( $stream ) { 230 | $this->streamResult = $stream; 231 | } 232 | 233 | /** 234 | * Set stream filters 235 | * @see http://php.net/manual/ru/stream.filters.php 236 | * @example array( 'string.strip_tags', 'string.tolower' ) 237 | * @param array $filters Registered Stream Filters 238 | * @return Client 239 | */ 240 | public function setStreamFilters( array $filters ) { 241 | $this->streamFilters = $filters; 242 | return $this; 243 | } 244 | 245 | /** 246 | * Enable headers in result. Default false 247 | * @param bool $enable 248 | */ 249 | public function enableHeaders($enable = true) { 250 | $this->enableHeaders = $enable; 251 | } 252 | /** 253 | * Set default curl options 254 | * @example: [ 255 | * CURLOPT_TIMEOUT => 10, 256 | * CURLOPT_COOKIEFILE => '/path/to/cookie.txt', 257 | * CURLOPT_COOKIEJAR => '/path/to/cookie.txt', 258 | * ... 259 | * ] 260 | * @param array $values 261 | */ 262 | public function setCurlOption($values) { 263 | foreach($values AS $key => $value) { 264 | $this->curlOptions[$key] = $value; 265 | } 266 | } 267 | 268 | /** 269 | * @see curl_share_setopt 270 | * @link http://php.net/manual/en/function.curl-share-setopt.php 271 | * @param $option 272 | * @param $value 273 | */ 274 | public function setShareOptions($option, $value) { 275 | if (!isset($this->sh)) { 276 | $this->sh = curl_share_init(); 277 | $this->setCurlOption(array(CURLOPT_SHARE => $this->sh)); 278 | } 279 | 280 | curl_share_setopt($this->sh, $option, $value); 281 | } 282 | 283 | /** 284 | * Max request in Asynchron query 285 | * @param $max int default:10 286 | * @return void 287 | */ 288 | public function setMaxRequest( $max ) { 289 | $this->maxRequest = $max; 290 | // PHP 5 >= 5.5.0 291 | if (function_exists('curl_multi_setopt')) { 292 | curl_multi_setopt($this->mh, CURLMOPT_MAXCONNECTS, $max); 293 | } 294 | } 295 | 296 | /** 297 | * @param $next int 298 | * @param $sleep float second 299 | * @param $blocking bool 300 | */ 301 | public function setSleep($next, $second = 1.0, $blocking = true) { 302 | $this->sleep = $second; 303 | $this->sleepNext = $next; 304 | $this->sleepBlocking = $blocking; 305 | } 306 | 307 | /** 308 | * Return count query 309 | * @return int 310 | */ 311 | public function getCountQuery() { 312 | return $this->queriesCount; 313 | } 314 | 315 | /** 316 | * Exec cURL resource 317 | * @return bool 318 | */ 319 | public function run() { 320 | if ( $this->isRunMh ) { 321 | $this->exec(); 322 | $this->execRead(); 323 | return ($this->processedQuery() || $this->queriesQueueCount > 0) ? true : ( $this->isRunMh = false ); 324 | } 325 | 326 | return $this->processedQuery(); 327 | } 328 | 329 | 330 | /** 331 | * Return all results; wait all request 332 | * @return Result|null 333 | */ 334 | public function all() { 335 | while($this->run()) {} 336 | $results = $this->results; 337 | $this->results = array(); 338 | return $results; 339 | } 340 | 341 | /** 342 | * Return one next result, wait first exec request 343 | * @return Result|null 344 | */ 345 | public function next() { 346 | while(empty($this->results) && $this->run()) {} 347 | return array_pop($this->results); 348 | } 349 | 350 | /** 351 | * Check has one result 352 | * @return bool 353 | */ 354 | public function has() { 355 | return !empty($this->results); 356 | } 357 | 358 | 359 | /** 360 | * Clear result request 361 | * @return void 362 | */ 363 | public function clear() { 364 | $this->results = array(); 365 | } 366 | 367 | /** 368 | * Set class result 369 | * @return void 370 | */ 371 | public function setClassResult($name) { 372 | $this->classResult = $name; 373 | } 374 | 375 | /** 376 | * @see $this->isSelect 377 | * @param bool $select 378 | */ 379 | public function isSelect($select) { 380 | $this->isSelect = $select; 381 | } 382 | 383 | protected function processedResponse($id) { 384 | $this->queriesQueueCount--; 385 | $this->count++; 386 | $query = $this->queriesQueue[$id]; 387 | 388 | $result = new $this->classResult($query); 389 | if (isset($query['id'])) { 390 | $this->results[$query['id']] = $result; 391 | } else { 392 | $this->results[] = $result; 393 | } 394 | 395 | curl_multi_remove_handle( $this->mh, $query['ch'] ); 396 | unset($this->queriesQueue[$id]); 397 | 398 | return true; 399 | } 400 | 401 | protected function processedQuery() { 402 | // not query 403 | if ( $this->queriesCount == 0 ) { 404 | return false; 405 | } 406 | 407 | $count = $this->maxRequest - $this->queriesQueueCount; 408 | 409 | if ($this->sleep !== 0) { 410 | $modulo_begin = $this->count % $this->sleepNext; 411 | $modulo_end = ($this->count + $count) % $this->sleepNext; 412 | 413 | $current_time = microtime(true); 414 | if (!isset($this->sleepNextTime)) { 415 | $this->sleepNextTime = $current_time - $this->sleep; 416 | } 417 | $sleep_time = (int) (($this->sleep - ($current_time - $this->sleepNextTime))*1000000); 418 | 419 | if ($sleep_time > 0) { 420 | if ($modulo_begin === 0) { 421 | if ($this->sleepBlocking) { 422 | usleep($sleep_time); 423 | $sleep_time = 0; 424 | $current_time = microtime(true); 425 | } else { 426 | $count = 0; 427 | } 428 | } elseif($modulo_begin >= $modulo_end) { 429 | $count-= $modulo_end; 430 | } 431 | } 432 | 433 | if ($sleep_time <= 0 && ($modulo_begin === 0 || $modulo_begin >= $modulo_end)) { 434 | $this->sleepNextTime = $current_time; 435 | } 436 | } 437 | 438 | if ($count > 0) { 439 | $limit = $this->queriesCount < $count ? $this->queriesCount : $count; 440 | 441 | $this->queriesCount-= $limit; 442 | $this->queriesQueueCount+= $limit; 443 | while($limit--) { 444 | $key = key($this->queries); 445 | $query = $this->queries[$key]; 446 | unset($this->queries[$key]); 447 | 448 | $query['ch'] = curl_init(); 449 | curl_setopt_array($query['ch'], $query['opts'] + $this->curlOptions); 450 | 451 | curl_multi_add_handle( $this->mh, $query['ch'] ); 452 | $id = $this->getResourceId( $query['ch'] ); 453 | $this->queriesQueue[$id] = $query; 454 | } 455 | } 456 | 457 | return $this->isRunMh = true; 458 | } 459 | 460 | protected function exec() { 461 | do { 462 | $mrc = curl_multi_exec( $this->mh, $active ); 463 | } while ( $mrc == CURLM_CALL_MULTI_PERFORM || ($this->isSelect && curl_multi_select( $this->mh, 0.01 ) > 0) ); 464 | } 465 | 466 | protected function execRead() { 467 | while(($info = curl_multi_info_read($this->mh, $active)) !== false) { 468 | if ( $info['msg'] === CURLMSG_DONE ) { 469 | $id = $this->getResourceId( $info['handle'] ); 470 | $this->processedResponse($id); 471 | } 472 | } 473 | } 474 | 475 | 476 | protected function getResourceId( $resource ) { 477 | return intval( $resource ); 478 | } 479 | 480 | public function __destruct() { 481 | if (isset($this->sh)) { 482 | curl_share_close($this->sh); 483 | } 484 | curl_multi_close( $this->mh ); 485 | } 486 | 487 | /** 488 | * @param string $baseUrl 489 | */ 490 | public function setBaseUrl($baseUrl) 491 | { 492 | $this->baseUrl = $baseUrl; 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | query = $query; 39 | } 40 | 41 | /** 42 | * Return id in request 43 | * @return null|mixed 44 | */ 45 | public function getId() { 46 | return isset($this->query['id']) ? $this->query['id'] : null; 47 | } 48 | 49 | /** 50 | * cURL session: curl_init() 51 | * @return resource 52 | */ 53 | public function getCh() { 54 | return $this->query['ch']; 55 | } 56 | 57 | /** 58 | * @see curl_getinfo(); 59 | * @return mixed 60 | */ 61 | public function getInfo() { 62 | return curl_getinfo($this->query['ch']); 63 | } 64 | 65 | /** 66 | * Return curl option in request 67 | * @return array 68 | */ 69 | public function getOptions() { 70 | $opts = $this->query['opts']; 71 | unset($opts[CURLOPT_FILE]); 72 | if (isset($opts[CURLOPT_WRITEHEADER])) { 73 | unset($opts[CURLOPT_WRITEHEADER]); 74 | } 75 | return $opts; 76 | } 77 | /** 78 | * Result http code 79 | * @see curl_getinfo($ch, CURLINFO_HTTP_CODE) 80 | * @return int 81 | */ 82 | public function getHttpCode() { 83 | return (int) curl_getinfo($this->query['ch'], CURLINFO_HTTP_CODE); 84 | } 85 | 86 | /** 87 | * Example: 88 | * $this->getHeaders() => 89 | * return [ 90 | * 'result' => 'HTTP/1.1 200 OK', 91 | * 'content-type' => 'text/html', 92 | * 'content-length' => '1024' 93 | * ... 94 | * ]; 95 | * 96 | * Or $this->headers['content-type'] => return 'text/html' @see $this->__get() 97 | * @return array 98 | */ 99 | public function getHeaders() { 100 | if (!isset($this->rawHeaders) && isset($this->query['opts'][CURLOPT_WRITEHEADER])) { 101 | rewind($this->query['opts'][CURLOPT_WRITEHEADER]); 102 | $headersRaw = stream_get_contents($this->query['opts'][CURLOPT_WRITEHEADER]); 103 | $headers = explode("\n", rtrim($headersRaw)); 104 | $this->rawHeaders['result'] = trim(array_shift($headers)); 105 | 106 | foreach ($headers AS $header) { 107 | list($name, $value) = array_map('trim', explode(':', $header, 2)); 108 | $name = strtolower($name); 109 | $this->rawHeaders[$name] = $value; 110 | } 111 | } 112 | return $this->rawHeaders; 113 | } 114 | 115 | /** 116 | * Result in request 117 | * @return string 118 | */ 119 | public function getBody() { 120 | if (isset($this->query['opts'][CURLOPT_FILE])) { 121 | rewind($this->query['opts'][CURLOPT_FILE]); 122 | return stream_get_contents($this->query['opts'][CURLOPT_FILE]); 123 | } else { 124 | return curl_multi_getcontent($this->query['ch']); 125 | } 126 | } 127 | 128 | /** 129 | * 130 | * @return mixed 131 | */ 132 | public function getBodyStream() { 133 | rewind($this->query['opts'][CURLOPT_FILE]); 134 | return $this->query['opts'][CURLOPT_FILE]; 135 | } 136 | 137 | /** 138 | * @see json_decode 139 | * @return mixed 140 | */ 141 | public function getJson() { 142 | $args = func_get_args(); 143 | if (empty($args)) { 144 | return @json_decode($this->getBody()); 145 | } else { 146 | array_unshift($args, $this->getBody()); 147 | return @call_user_func_array('json_decode', $args); 148 | } 149 | } 150 | 151 | /** 152 | * return params request 153 | * @return mixed 154 | */ 155 | public function getParams() { 156 | return $this->query['params']; 157 | } 158 | 159 | 160 | /** 161 | * Has error 162 | * @param null|string $type use: network|http 163 | * @return bool 164 | */ 165 | public function hasError($type = null) { 166 | $errorType = $this->getErrorType(); 167 | return (isset($errorType) && ($errorType == $type || !isset($type))); 168 | } 169 | 170 | /** 171 | * Return network if has curl error or http if http code >=400 172 | * @return null|string return string: network|http or null if not error 173 | */ 174 | public function getErrorType() { 175 | if (curl_error($this->query['ch'])) { 176 | return 'network'; 177 | } 178 | 179 | if ($this->getHttpCode() >= 400) { 180 | return 'http'; 181 | } 182 | 183 | return null; 184 | } 185 | 186 | /** 187 | * Return message error 188 | * @return null|string 189 | */ 190 | public function getError() { 191 | $message = null; 192 | switch($this->getErrorType()) { 193 | case 'network': 194 | $message = curl_error($this->query['ch']); 195 | break; 196 | case 'http': 197 | $message = 'http error ' . $this->getHttpCode(); 198 | break; 199 | } 200 | return $message; 201 | } 202 | 203 | /** 204 | * Return code error 205 | * @return int|null 206 | */ 207 | public function getErrorCode() { 208 | $number = null; 209 | switch($this->getErrorType()) { 210 | case 'network': 211 | $number = (int) curl_errno($this->query['ch']); 212 | break; 213 | case 'http': 214 | $number = $this->getHttpCode(); 215 | break; 216 | } 217 | return $number; 218 | } 219 | 220 | public function __toString() { 221 | return $this->getBody(); 222 | } 223 | 224 | /** 225 | * Simple get result 226 | * @Example: $this->id, $this->body, $this->error, $this->hasError, $this->headers['content-type'], ... 227 | * 228 | * @param $key 229 | * @return null 230 | */ 231 | public function __get($key) { 232 | $method = 'get' . $key; 233 | return method_exists($this, $method) ? $this->$method() : null; 234 | } 235 | 236 | public function __destruct() { 237 | if (isset($this->query['opts'][CURLOPT_FILE]) && is_resource($this->query['opts'][CURLOPT_FILE])) { 238 | fclose($this->query['opts'][CURLOPT_FILE]); 239 | } 240 | 241 | if (isset($this->query['opts'][CURLOPT_WRITEHEADER]) && is_resource($this->query['opts'][CURLOPT_WRITEHEADER])) { 242 | fclose($this->query['opts'][CURLOPT_WRITEHEADER]); 243 | } 244 | 245 | if (is_resource($this->query['ch'])) { 246 | curl_close($this->query['ch']); 247 | } 248 | } 249 | } -------------------------------------------------------------------------------- /tests/CaseTest.php: -------------------------------------------------------------------------------- 1 | domain = $domain; 21 | } 22 | $this->req = $this->createReq(); 23 | } 24 | 25 | protected function createReq() { 26 | $req = new Client(); 27 | $req->setMaxRequest(10); 28 | return $req; 29 | } 30 | 31 | protected function url($path) { 32 | return 'http://' . $this->domain . $path; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/QueryTest.php: -------------------------------------------------------------------------------- 1 | url('/simple-0.txt'), 12 | $this->url('/simple-1.txt'), 13 | $this->url('/simple-2.txt'), 14 | $this->url('/simple-3.txt'), 15 | ); 16 | 17 | foreach($urls AS $url) { 18 | $this->req->add(array( 19 | CURLOPT_URL => $url 20 | )); 21 | } 22 | 23 | while ($result = $this->req->next()) { 24 | $i++; 25 | $this->assertEquals(200, $result->getHttpCode()); 26 | $this->assertNotEmpty($result->getBody()); 27 | } 28 | 29 | $this->assertEquals(count($urls), $i); 30 | } 31 | 32 | public function testDo() { 33 | $urls = array( 34 | $this->url('/simple-0.txt'), 35 | $this->url('/simple-1.txt'), 36 | $this->url('/simple-2.txt'), 37 | $this->url('/simple-3.txt'), 38 | ); 39 | 40 | foreach($urls AS $url) { 41 | $this->req->add(array( 42 | CURLOPT_URL => $url 43 | )); 44 | } 45 | 46 | do{ 47 | while($this->req->has()) { 48 | $result = $this->req->next(); 49 | $this->assertEquals(200, $result->getHttpCode()); 50 | $this->assertNotEmpty($result->getBody()); 51 | } 52 | }while($this->req->run()); 53 | } 54 | 55 | public function testSpider() { 56 | $this->req->setMaxRequest(10); 57 | 58 | $this->req->add(array( 59 | CURLOPT_URL => $this->url('/simple-1.txt'), 60 | )); 61 | 62 | $this->req->add(array( 63 | CURLOPT_URL => $this->url('/simple-2.txt'), 64 | )); 65 | 66 | while($this->req->run() || $this->req->has()) { 67 | while($this->req->has()) { 68 | $result = $this->req->next(); 69 | 70 | $num = (int) substr($result->getBody(), -1, 1); 71 | if ($num > 4) { 72 | continue; 73 | } 74 | 75 | for($i=0; $ireq->add(array( 77 | CURLOPT_URL => $this->url('/simple-'.($num+1).'.txt'), 78 | )); 79 | } 80 | 81 | $this->assertEquals(200, $result->getHttpCode()); 82 | $this->assertNotEmpty($result->getBody()); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /tests/SimpleTest.php: -------------------------------------------------------------------------------- 1 | req->setMaxRequest(11); 14 | $this->req->setSleep($sleep_num, $sleep_time); 15 | 16 | 17 | $urls = array(); 18 | for($i=0; $i<$count;$i++) { 19 | $urls[] = $this->url('/simple-'.$i.'.txt'); 20 | } 21 | 22 | $time_begin = microtime(true); 23 | $results = $this->req->get($urls); 24 | $time_end = microtime(true); 25 | 26 | $count_sleep = intval($count/$sleep_num) - ($count%$sleep_num == 0 ? 1 : 0); 27 | $script_time_sleep = $count_sleep * $sleep_time; 28 | 29 | $script_time_running = $time_end - $time_begin; 30 | 31 | $this->assertEquals($count, count($results)); 32 | $this->assertTrue($script_time_running >= $script_time_sleep); 33 | } 34 | 35 | 36 | public function testGet() { 37 | $result = $this->req->get($this->url('/simple.txt')); 38 | $this->assertEquals('simple', $result->getBody()); 39 | $this->assertEquals(200, $result->getHttpCode()); 40 | } 41 | 42 | public function testGetWithBaseUrl() { 43 | $this->req->setBaseUrl('http://' . $this->domain); 44 | 45 | $result = $this->req->get('/simple.txt'); 46 | $this->assertEquals('simple', $result->getBody()); 47 | $this->assertEquals(200, $result->getHttpCode()); 48 | } 49 | 50 | public function testHeaders() { 51 | $this->req->enableHeaders(); 52 | $result = $this->req->get($this->url('/simple.txt')); 53 | $this->assertNotEmpty($result->getHeaders()); 54 | $this->assertNotEmpty($result->headers['server']); 55 | } 56 | 57 | public function testGetAsynchron() { 58 | $urls = array( 59 | $this->url('/simple-sleep-4.txt'), 60 | $this->url('/simple-sleep-1.txt'), 61 | $this->url('/simple-sleep-2.txt'), 62 | $this->url('/simple-sleep-3.txt'), 63 | ); 64 | $time_begin = time(); 65 | $results = $this->req->get($urls); 66 | $time_end = time(); 67 | 68 | $i=1; 69 | foreach($results AS $result) { 70 | $this->assertEquals('sleep-' .$i , $result->getBody()); 71 | $i++; 72 | } 73 | 74 | $this->assertEquals(4, count($results)); 75 | $this->assertTrue(($time_end - $time_begin) < 6); 76 | } 77 | 78 | 79 | 80 | public function testGetSynchron() { 81 | $req = $this->createReq(); 82 | $req->setMaxRequest(1); 83 | 84 | $urls = array( 85 | $this->url('/simple-sleep-1.txt'), 86 | $this->url('/simple-sleep-2.txt'), 87 | $this->url('/simple-sleep-3.txt'), 88 | ); 89 | 90 | $time_begin = time(); 91 | $results = $req->get($urls); 92 | $time_end = time(); 93 | 94 | foreach($results AS $i => $result) { 95 | $this->assertEquals('sleep-' . ($i+1), $result->getBody()); 96 | } 97 | 98 | $this->assertEquals(3, count($results)); 99 | $this->assertTrue(($time_end - $time_begin) >= 6); 100 | } 101 | 102 | public function testPost() { 103 | $result = $this->req->post($this->url('/post.php'), array('data' => 'post data')); 104 | $this->assertEquals('POST', (string) $result); 105 | } 106 | 107 | public function testPostWithBaseUrl() { 108 | $this->req->setBaseUrl('http://' . $this->domain); 109 | 110 | $result = $this->req->post('/post.php', array('data' => 'post data')); 111 | $this->assertEquals('POST', (string) $result); 112 | } 113 | 114 | 115 | public function testHttp404() { 116 | $result = $this->req->get($this->url('/simple-error/404.txt')); 117 | $this->assertTrue($result->hasError()); 118 | $this->assertEquals(404, $result->getErrorCode()); 119 | $this->assertEquals('http', $result->getErrorType()); 120 | $this->assertNotEmpty($result->getError()); 121 | } 122 | 123 | public function testHttp503() { 124 | $result = $this->req->get($this->url('/simple-error/503.txt')); 125 | $this->assertTrue($result->hasError()); 126 | $this->assertEquals(503, $result->getErrorCode()); 127 | $this->assertEquals('http', $result->getErrorType()); 128 | $this->assertNotEmpty($result->getError()); 129 | } 130 | 131 | public function testTimeout() { 132 | $url = $this->url('/simple-sleep-3.txt'); 133 | $timeOut = 1; 134 | $result = $this->req->get($url, array(CURLOPT_TIMEOUT => $timeOut)); 135 | $this->assertTrue($result->hasError()); 136 | $this->assertEquals('network', $result->getErrorType()); 137 | $this->assertNotEmpty($result->getError()); 138 | 139 | $this->assertEquals($timeOut, $result->options[CURLOPT_TIMEOUT]); 140 | $this->assertEquals($url, $result->options[CURLOPT_URL]); 141 | } 142 | 143 | public function testJson() { 144 | $result = $this->req->get($this->url('/simple-json.txt')); 145 | $data = $result->getJson(true); 146 | $this->assertNotEmpty($data); 147 | $this->assertArrayHasKey('json_test', $data); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/data/travis_nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | include mime.types; 9 | default_type application/octet-stream; 10 | 11 | gzip on; 12 | 13 | server { 14 | server_name localhost; 15 | listen 127.0.0.1:80; 16 | 17 | # simple 18 | location = /simple.txt { 19 | echo -n "simple"; 20 | } 21 | 22 | # json 23 | location = /simple-json.txt { 24 | echo -n '{"json_test":1}'; 25 | } 26 | 27 | # asynchron 28 | location ~ /simple-([0-9]+).txt$ { 29 | echo -n "simple-"; 30 | echo -n $1; 31 | } 32 | 33 | # sleep 34 | location ~ /simple-sleep-([0-9\.]+).txt$ { 35 | echo_sleep $1; 36 | echo -n "sleep-"; 37 | echo -n $1; 38 | } 39 | 40 | # error 41 | location = /simple-error/404.txt { 42 | return 404; 43 | } 44 | 45 | location = /simple-error/503.txt { 46 | return 503; 47 | } 48 | 49 | location = /simple-error/timeout.txt { 50 | echo_sleep 5; 51 | echo -n "timeout"; 52 | } 53 | 54 | 55 | # post 56 | location = /post.php { 57 | echo -n $request_method; 58 | } 59 | 60 | # file 61 | location = /file.php { 62 | echo -n $request_body_file; 63 | } 64 | } 65 | } --------------------------------------------------------------------------------