├── .gitignore ├── .php.ext.ini ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── diff.webp ├── multi-http.jpg ├── phpunit.xml ├── src ├── Exception │ ├── InvalidArgumentException.php │ ├── InvalidOperationException.php │ └── UnexpectedResponseException.php ├── Handler │ ├── Form.php │ ├── IHandler.php │ ├── Json.php │ └── Upload.php ├── Helper.php ├── Http.php ├── Mime.php ├── MultiRequest.php ├── Request.php └── Response.php ├── test.md ├── tests ├── MultiRequestTest.php ├── RequestTest.php ├── bootstrap-server.php ├── bootstrap.php ├── dynamic │ └── all_data.php └── static │ ├── test.json │ └── test_image.jpg └── usage.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.php.ext.ini: -------------------------------------------------------------------------------- 1 | date.timezone = "Asia/Shanghai" 2 | default_socket_timeout = 120 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: required 3 | dist: trusty 4 | group: edge 5 | 6 | php: 7 | - '5.4' 8 | - '5.5' 9 | - '5.6' 10 | - '7.0' 11 | - '7.1' 12 | 13 | before_script: 14 | # - phpenv config-add .php.ext.ini 15 | # - composer self-update 16 | # - composer clear 17 | - composer install --prefer-source --no-interaction 18 | 19 | script: phpunit 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiHttp 2 | 3 | [![](https://api.travis-ci.org/sinacms/MultiHttp.svg?branch=master)](https://travis-ci.org/sinacms/MultiHttp) 4 | [![](https://scrutinizer-ci.com/g/sinacms/MultiHttp/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/sinacms/MultiHttp) 5 | [![](https://scrutinizer-ci.com/g/sinacms/MultiHttp/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/sinacms/MultiHttp/) 6 | 7 | 8 |    This is high performance curl wrapper written in pure PHP. 9 | It's compatible with PHP 5.4+ and HHVM. 10 | Notice that libcurl version must be over 7.36.0, otherwise timeout can not suppert decimal. 11 | 12 |    这是一个高性能的PHP封装的HTTP Restful多线程并发请求库,参考借鉴了httpful 、multirequest等优秀的代码。它与PHP 5.4和hhvm兼容。 13 | 注意,libcurl版本必须>=7.36.0,否则超时不支持小数。 14 | 15 | ## 请使用最新 Tag ( Please use the latest tag version) 16 | 17 | 18 | ![](multi-http.jpg) 19 | ![](diff.webp) 20 | 21 | ## Contents 22 | 23 | * [Feature](#feature) 24 | * [Installation](#installation) 25 | * [Usage](#usage) 26 | * [Single-request](single-request) 27 | * [Multi-request](multi-request) 28 | * [Documentation](#documentation) 29 | * [Request](#request) 30 | * [MultiRequest](#multiRequest) 31 | 32 | 33 | 34 | 35 | 36 | ## Feature 37 | - alias of curl option, e.g. 'timeout' equals 'CURLOPT_TIMEOUT' etc. 38 | - Request and MultiRequest class , can be used in any combination 39 | - graceful and efficient 40 | 41 | ## Installation 42 | 43 | You can use composer to install this library from the command line. 44 | ```bash 45 | composer require sinacms/multihttp 46 | ``` 47 | 48 | 49 | ## Usage 50 | 51 | ### Single-request: 52 | 53 | 54 | ```php 55 | get('http://sina.com.cn', array( 63 | 'timeout' => 3, 64 | 'expects_mime'=>'json', 65 | 'retry_times' => 3, 66 | 'ip' => '127.0.0.1:8080',//alias for proxy, better than raw proxy 67 | 'callback' => function (Response $response) { 68 | echo $response->body; 69 | }, 70 | ))->send(); 71 | 72 | //method 2 73 | $result = Request::create()->post( 74 | 'http://127.0.0.1',array('data'=>'this_is_post_data'), array( 75 | 'callback' => function (Response $response) { 76 | echo $response->body; 77 | }))->send(); 78 | 79 | ``` 80 | 81 | 82 | ### Multi-request: 83 | 84 | ```php 85 | add('GET', 'http://sina.cn',array(), array( 93 | 'timeout' => 3, 94 | 'callback' => function (Response $response) { 95 | echo $response->body; 96 | } 97 | )) 98 | ->add('GET', 'http://google.cn',array(), array( 99 | 'timeout' => 3, 100 | 'callback' => function (Response $response) { 101 | echo $response->body; 102 | } 103 | )) 104 | ->sendAll(); 105 | 106 | ?> 107 | ``` 108 | 109 | ## Documentation 110 | * ### Request 111 | * #### option shorthand 112 | 'url' => 'CURLOPT_URL', 113 | 'debug' => 'CURLOPT_VERBOSE',//for debug verbose 114 | 'method' => 'CURLOPT_CUSTOMREQUEST', 115 | 'data' => 'CURLOPT_POSTFIELDS', // array or string , file begin with '@' 116 | 'ua' => 'CURLOPT_USERAGENT', 117 | 'timeout' => 'CURLOPT_TIMEOUT', // (secs) 0 means indefinitely 118 | 'connect_timeout' => 'CURLOPT_CONNECTTIMEOUT', 119 | 'referer' => 'CURLOPT_REFERER', 120 | 'binary' => 'CURLOPT_BINARYTRANSFER', 121 | 'port' => 'CURLOPT_PORT', 122 | 'header' => 'CURLOPT_HEADER', // TRUE:include header 123 | 'headers' => 'CURLOPT_HTTPHEADER', // array 124 | 'download' => 'CURLOPT_FILE', // writing file stream (using fopen()), default is STDOUT 125 | 'upload' => 'CURLOPT_INFILE', // reading file stream 126 | 'transfer' => 'CURLOPT_RETURNTRANSFER', // TRUE:return string; FALSE:output directly (curl_exec) 127 | 'follow_location' => 'CURLOPT_FOLLOWLOCATION', 128 | 'timeout_ms' => 'CURLOPT_TIMEOUT_MS', // milliseconds, libcurl version > 7.36.0 , 129 | 130 | * public static function create() 131 | * public function endCallback() 132 | * public function hasEndCallback() 133 | * public function onEnd(callable$callback) 134 | * public function uri 135 | * public function getIni($field) 136 | * public function addQuery($data) 137 | * public function post($uri, array $payload = array(), array $options = array()) 138 | * public function addOptions(array $options = array()) 139 | * public function get($uri, array $options = array()) 140 | * public function send() 141 | * public function applyOptions() 142 | * public function makeResponse($isMultiCurl = false) 143 | * ### MultiRequest 144 | * public static function create() 145 | * public function addOptions(array $URLOptions) 146 | * public function add($method, $uri, array $payload = array(), array $options = array()) 147 | * public function import(Request $request) 148 | * public function sendAll() 149 | 150 | 151 | [More][https://github.com/sinacms/MultiHttp/blob/master/usage.md](https://github.com/sinacms/MultiHttp/blob/master/usage.md) 152 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sinacms/multihttp", 3 | "description": "a tool for multi-curl written in php", 4 | "keywords": ["php","multi", "curl", "parallel", "concurrency", "async", "threads", "requests", "asynchronous", "http","rest", "restful", "api"], 5 | "type": "library", 6 | "repositories": [ 7 | ], 8 | "authors": [ 9 | { 10 | "name": "george wu", 11 | "email": "sunsky303@gmail.com" 12 | } 13 | ], 14 | "license": "Apache-2.0", 15 | "require": { 16 | "php": ">=5.4.0", 17 | "ext-curl": "*", 18 | "lib-curl": "*" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "~4.0" 22 | }, 23 | "autoload": { 24 | "psr-4": {"MultiHttp\\": "src"} 25 | }, 26 | "minimum-stability": "dev" 27 | } 28 | -------------------------------------------------------------------------------- /diff.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinacms/MultiHttp/0f245757c9fa5a6f2e4ac9a6e6e04492a8df756e/diff.webp -------------------------------------------------------------------------------- /multi-http.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinacms/MultiHttp/0f245757c9fa5a6f2e4ac9a6e6e04492a8df756e/multi-http.jpg -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | vendor 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | self::HEAD, 30 | 'GET' => self::GET, 31 | 'POST' => self::POST, 32 | 'PUT' => self::PUT, 33 | 'DELETE' => self::DELETE, 34 | 'PATCH' => self::PATCH, 35 | 'OPTIONS' => self::OPTIONS, 36 | 'TRACE' => self::TRACE, 37 | ); 38 | 39 | /** 40 | * @param $uri 41 | * @param null $payload 42 | * @param array $options 43 | * @return mixed 44 | */ 45 | abstract function post($uri, $payload = null, array $options = array()); 46 | 47 | /** 48 | * @param $uri 49 | * @param null $payload 50 | * @param array $options 51 | * @return mixed 52 | */ 53 | abstract function patch($uri, $payload = null, array $options = array()); 54 | 55 | /** 56 | * @param $uri 57 | * @param null $payload 58 | * @param array $options 59 | * @return mixed 60 | */ 61 | abstract function put($uri, $payload = null, array $options = array()); 62 | 63 | /** 64 | * @param $uri 65 | * @param array $options 66 | * @return mixed 67 | */ 68 | abstract function get($uri, array $options = array()); 69 | 70 | /** 71 | * @param $uri 72 | * @param array $options 73 | * @return mixed 74 | */ 75 | abstract function head($uri, array $options = array()); 76 | 77 | /** 78 | * @param $uri 79 | * @param array $options 80 | * @return mixed 81 | */ 82 | abstract function delete($uri, array $options = array()); 83 | 84 | /** 85 | * @param $uri 86 | * @param array $options 87 | * @return mixed 88 | */ 89 | abstract function options($uri, array $options = array()); 90 | 91 | /** 92 | * @param $uri 93 | * @param array $options 94 | * @return mixed 95 | */ 96 | abstract function trace($uri, array $options = array()); 97 | 98 | /** 99 | * @param $method 100 | * @return bool 101 | */ 102 | public static function hasBody($method){ 103 | return in_array($method, array(self::POST, self::PUT, self::PATCH, self::OPTIONS)); 104 | } 105 | } -------------------------------------------------------------------------------- /src/Mime.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Mime 11 | { 12 | const JSON = 'application/json'; 13 | const XML = 'application/xml'; 14 | const XHTML = 'application/html+xml'; 15 | const FORM = 'application/x-www-form-urlencoded'; 16 | const UPLOAD = 'multipart/form-data'; 17 | const PLAIN = 'text/plain'; 18 | const JS = 'text/javascript'; 19 | const HTML = 'text/html'; 20 | const YAML = 'application/x-yaml'; 21 | const CSV = 'text/csv'; 22 | 23 | /** 24 | * Map short name for a mime type 25 | * to a full proper mime type 26 | */ 27 | public static $mimes = array( 28 | 'json' => self::JSON, 29 | 'xml' => self::XML, 30 | 'form' => self::FORM, 31 | 'plain' => self::PLAIN, 32 | 'text' => self::PLAIN, 33 | 'upload' => self::UPLOAD, 34 | 'html' => self::HTML, 35 | 'xhtml' => self::XHTML, 36 | 'js' => self::JS, 37 | 'javascript' => self::JS, 38 | 'yaml' => self::YAML, 39 | 'csv' => self::CSV, 40 | ); 41 | 42 | /** 43 | * Get the full Mime Type name from a "short name". 44 | * Returns the short if no mapping was found. 45 | * @param string $short_name common name for mime type (e.g. json) 46 | * @return string full mime type (e.g. application/json) 47 | */ 48 | public static function getFullMime($short_name) 49 | { 50 | return array_key_exists($short_name, self::$mimes) ? self::$mimes[$short_name] : $short_name; 51 | } 52 | 53 | /** 54 | * @param string $short_name 55 | * @return bool 56 | */ 57 | public static function supportsMimeType($short_name) 58 | { 59 | return array_key_exists($short_name, self::$mimes); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/MultiRequest.php: -------------------------------------------------------------------------------- 1 | defaultOptions = $options; 51 | return $this; 52 | } 53 | protected static function prepare() 54 | { 55 | self::$multiHandler = curl_multi_init(); 56 | } 57 | 58 | /** 59 | * @param $method 60 | * @param $uri 61 | * @param $payload 62 | * @param array $options 63 | * @return $this 64 | */ 65 | public function add($method, $uri, $payload, array $options = array()) 66 | { 67 | $options = array( 68 | 'method' => $method, 69 | 'url' => $uri, 70 | 'data' => $payload, 71 | ) + $options; 72 | $this->addOptions(array($options)); 73 | return $this; 74 | } 75 | 76 | /** 77 | * @param array $URLOptions 78 | * example: array(array('url'=>'http://localhost:9999/','timeout'=>1, 'method'=>'POST', 'data'=>'aa=bb&c=d')) 79 | * @return $this 80 | */ 81 | public function addOptions(array $URLOptions) 82 | { 83 | foreach ($URLOptions as $options) { 84 | $options = $options + $this->defaultOptions; 85 | $request = Request::create()->addOptions($options)->applyOptions(); 86 | if (isset($options['callback'])) { 87 | $request->onEnd($options['callback']); 88 | } 89 | $this->import($request); 90 | } 91 | return $this; 92 | } 93 | 94 | /** 95 | * @param Request $request 96 | * @return $this 97 | */ 98 | public function import(Request $request) 99 | { 100 | if (!is_resource($request->curlHandle)) { 101 | throw new InvalidArgumentException('Request curl handle is not initialized'); 102 | } 103 | curl_multi_add_handle(self::$multiHandler, $request->curlHandle); 104 | self::$requestPool[] = $request; 105 | return $this; 106 | } 107 | 108 | /** 109 | * @return array(Response) 110 | */ 111 | public function sendAll() 112 | { 113 | $sleepTime = 1000;//microsecond, prevent CPU 100% 114 | do { 115 | curl_multi_exec(self::$multiHandler, $active); 116 | // bug in PHP 5.3.18+ where curl_multi_select can return -1 117 | // https://bugs.php.net/bug.php?id=63411 118 | if (curl_multi_select(self::$multiHandler) == -1) { 119 | usleep($sleepTime); 120 | } 121 | usleep($sleepTime); 122 | } while ($active); 123 | $return = array(); 124 | foreach (self::$requestPool as $request) { 125 | $return[] = $request->send(true); 126 | curl_multi_remove_handle(self::$multiHandler, $request->curlHandle); 127 | curl_close($request->curlHandle); 128 | } 129 | curl_multi_close(self::$multiHandler); 130 | self::$requestPool = array(); 131 | self::$multiHandler = null; 132 | return $return; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | 'CURLOPT_URL', 30 | 'uri' => 'CURLOPT_URL', 31 | 'debug' => 'CURLOPT_VERBOSE',//for debug verbose 32 | 'method' => 'CURLOPT_CUSTOMREQUEST', 33 | 'data' => 'CURLOPT_POSTFIELDS', // array or string , file begin with '@' 34 | 'ua' => 'CURLOPT_USERAGENT', 35 | 'timeout' => 'CURLOPT_TIMEOUT', // (secs) 0 means indefinitely 36 | 'connect_timeout' => 'CURLOPT_CONNECTTIMEOUT', 37 | 'referer' => 'CURLOPT_REFERER', 38 | 'binary' => 'CURLOPT_BINARYTRANSFER', 39 | 'port' => 'CURLOPT_PORT', 40 | 'header' => 'CURLOPT_HEADER', // TRUE:include header 41 | 'headers' => 'CURLOPT_HTTPHEADER', // array 42 | 'download' => 'CURLOPT_FILE', // writing file stream (using fopen()), default is STDOUT 43 | 'upload' => 'CURLOPT_INFILE', // reading file stream 44 | 'transfer' => 'CURLOPT_RETURNTRANSFER', // TRUE:return string; FALSE:output directly (curl_exec) 45 | 'follow_location' => 'CURLOPT_FOLLOWLOCATION', 46 | 'timeout_ms' => 'CURLOPT_TIMEOUT_MS', // milliseconds, libcurl version > 7.36.0 , 47 | 'expects_mime' => null, //expected mime 48 | 'send_mime' => null, //send mime 49 | 'ip' => null,//specify ip to send request 50 | 'callback' => null,//callback on end 51 | 52 | ); 53 | protected static $loggerHandler; 54 | public 55 | $curlHandle, 56 | $uri, 57 | $sendMime, 58 | $expectsMime, 59 | $timeout, 60 | $maxRedirects, 61 | $encoding, 62 | $payload, 63 | $retryTimes, 64 | /** 65 | * @var int seconds 66 | */ 67 | $retryDuration, 68 | $followRedirects; 69 | 70 | protected 71 | $body, 72 | $endCallback, 73 | $withURIQuery, 74 | $hasInitialized = false, 75 | /** 76 | * @var array 77 | */ 78 | $options = array( 79 | 'CURLOPT_MAXREDIRS' => 10, 80 | 'CURLOPT_SSL_VERIFYPEER' => false,//for https 81 | 'CURLOPT_SSL_VERIFYHOST' => 0,//for https 82 | 'CURLOPT_IPRESOLVE' => CURL_IPRESOLVE_V4,//ipv4 first 83 | 'CURLOPT_SAFE_UPLOAD' => false,// compatible with PHP 5.6.0 84 | 'CURLOPT_USERAGENT' => 'Mozilla/5.0 (compatible;)', 85 | 'header' => true, 86 | 'method' => self::GET, 87 | 'transfer' => true, 88 | 'headers' => array(), 89 | 'follow_location' => true, 90 | 'timeout' => 0, 91 | // 'ip' => null, //host, in string, .e.g: 172.16.1.1:888 92 | 'retry_times' => 1,//redo task when failed 93 | 'retry_duration' => 0,//in seconds 94 | 'send_mime' => 'form',//in seconds 95 | ); 96 | 97 | 98 | /** 99 | * Request constructor. 100 | */ 101 | protected function __construct() 102 | { 103 | } 104 | 105 | /** 106 | * @return Request 107 | */ 108 | public static function create() 109 | { 110 | return new self; 111 | } 112 | 113 | /** 114 | * @param callable $handler 115 | */ 116 | public static function setLogHandler(callable $handler) 117 | { 118 | self::$loggerHandler = $handler; 119 | } 120 | /** 121 | * Specify timeout 122 | * @param float|int $timeout seconds to timeout the HTTP call 123 | * @return Request 124 | */ 125 | public function timeout($timeout) 126 | { 127 | $this->timeout = $timeout; 128 | return $this; 129 | } 130 | 131 | /** 132 | * @return Request 133 | */ 134 | public function noFollow() 135 | { 136 | return $this->follow(0); 137 | } 138 | 139 | /** 140 | * If the response is a 301 or 302 redirect, automatically 141 | * send off another request to that location 142 | * @param int $follow follow or not to follow or maximal number of redirects 143 | * @return Request 144 | */ 145 | public function follow($follow) 146 | { 147 | $this->maxRedirects = abs($follow); 148 | $this->followRedirects = $follow > 0; 149 | return $this; 150 | } 151 | 152 | /** 153 | * @param $parsedComponents 154 | * @return string 155 | */ 156 | private static function combineUrl($parsedComponents) 157 | { 158 | $scheme = isset($parsedComponents['scheme']) ? $parsedComponents['scheme'] . '://' : ''; 159 | $host = isset($parsedComponents['host']) ? $parsedComponents['host'] : ''; 160 | $port = isset($parsedComponents['port']) ? ':' . $parsedComponents['port'] : ''; 161 | $user = isset($parsedComponents['user']) ? $parsedComponents['user'] : ''; 162 | $pass = isset($parsedComponents['pass']) ? ':' . $parsedComponents['pass'] : ''; 163 | $pass = ($user || $pass) ? "$pass@" : ''; 164 | $path = isset($parsedComponents['path']) ? $parsedComponents['path'] : ''; 165 | $query = isset($parsedComponents['query']) ? '?' . $parsedComponents['query'] : ''; 166 | $fragment = isset($parsedComponents['fragment']) ? '#' . $parsedComponents['fragment'] : ''; 167 | return "$scheme$user$pass$host$port$path$query$fragment"; 168 | } 169 | 170 | /** 171 | * @param string $mime 172 | * @return $this 173 | */ 174 | public function expectsMime($mime = 'json') 175 | { 176 | $this->expectsMime = $mime; 177 | $this->options['expects_mime'] = $mime; 178 | return $this; 179 | } 180 | 181 | /** 182 | * @param string $mime 183 | * @return Request 184 | */ 185 | public function sendMime($mime = 'json') 186 | { 187 | $this->sendMime = $mime; 188 | $this->options['send_mime'] = $mime; 189 | // $this->addHeader('Content-type', Mime::getFullMime($mime)); 190 | return $this; 191 | } 192 | 193 | /** 194 | * @param $headerName 195 | * @param $value , can be rawurlencode 196 | * @return $this 197 | */ 198 | public function addHeader($headerName, $value) 199 | { 200 | $this->options['headers'][] = $headerName . ': ' . $value; 201 | return $this; 202 | } 203 | 204 | /** 205 | * @param $uri 206 | * @return $this 207 | */ 208 | public function uri($uri) 209 | { 210 | $this->uri = $uri; 211 | return $this; 212 | } 213 | 214 | 215 | 216 | /** 217 | * @param array $headers 218 | * @return $this 219 | */ 220 | public function addHeaders(array $headers) 221 | { 222 | foreach ($headers as $header => $value) { 223 | $this->addHeader($header, $value); 224 | } 225 | return $this; 226 | } 227 | /** 228 | * @return mixed 229 | */ 230 | public function endCallback() 231 | { 232 | return $this->endCallback; 233 | } 234 | 235 | /** 236 | * @return bool 237 | */ 238 | public function hasEndCallback() 239 | { 240 | return isset($this->endCallback); 241 | } 242 | 243 | /** 244 | * @param $field alias or field name 245 | * @return bool|mixed 246 | */ 247 | public function getIni($field = null) 248 | { 249 | if(!$field) return $this->options; 250 | $full = self::fullOption($field); 251 | return isset($this->options[$full]) ? $this->options[$full] : false; 252 | } 253 | 254 | /** 255 | * @param $key 256 | * @return mixed 257 | */ 258 | protected static function fullOption($key) 259 | { 260 | $full = false; 261 | if (isset(self::$curlAlias[$key])) { 262 | $full = self::$curlAlias[$key]; 263 | } elseif ((substr($key, 0, strlen('CURLOPT_')) == 'CURLOPT_') && defined($key)) { 264 | $full = $key; 265 | } 266 | return $full; 267 | } 268 | 269 | /** 270 | * @param $queryData 271 | * @return $this 272 | */ 273 | public function addQuery($queryData) 274 | { 275 | if (!empty($queryData)) { 276 | if (is_array($queryData)) { 277 | $this->withURIQuery = http_build_query($queryData); 278 | } else if (is_string($queryData)) { 279 | $this->withURIQuery = $queryData; 280 | } else { 281 | throw new InvalidArgumentException('data must be array or string'); 282 | } 283 | } 284 | return $this; 285 | } 286 | /** 287 | * @param $uri 288 | * @param null $payload 289 | * @param array $options 290 | * @return Request 291 | */ 292 | public function post($uri, $payload = null, array $options = array()) 293 | { 294 | return $this->ini(Http::POST, $uri, $payload, $options); 295 | } 296 | 297 | /** 298 | * @param $uri 299 | * @param null $payload 300 | * @param array $options 301 | * @return Request 302 | */ 303 | public function upload($uri, $payload = null, array $options = array()) 304 | { 305 | return $this->ini(Http::POST, $uri, $payload, $options)->sendMime('upload'); 306 | } 307 | 308 | /** 309 | * @param $uri 310 | * @param null $payload 311 | * @param array $options 312 | * @param null $response 313 | * @return string 314 | */ 315 | public function quickPost($uri, $payload = null, array $options = array(), &$response = null) 316 | { 317 | $response = $this->post($uri, $payload, $options)->send(); 318 | return $response->body; 319 | } 320 | 321 | 322 | /** 323 | * @param $method 324 | * @param $url 325 | * @param $data 326 | * @param array $options 327 | * @return $this 328 | */ 329 | protected function ini($method, $url, $data, array $options = array()) 330 | { 331 | $options = array('url' => $url, 'method' => $method, 'data' => $data) + $options; 332 | $this->addOptions($options); 333 | 334 | return $this; 335 | } 336 | 337 | /** 338 | * @param array $options 339 | * @return $this 340 | */ 341 | public function addOptions(array $options = array()) 342 | { 343 | $this->options = $options + $this->options; 344 | $this->uri = $this->options['url']; 345 | return $this; 346 | } 347 | 348 | /** 349 | * @param $uri 350 | * @param null $payload 351 | * @param array $options 352 | * @return Request 353 | */ 354 | function put($uri, $payload = null, array $options = array()) 355 | { 356 | return $this->ini(Http::PUT, $uri, $payload, $options); 357 | } 358 | 359 | /** 360 | * @param $uri 361 | * @param null $payload 362 | * @param array $options 363 | * @return Request 364 | */ 365 | function patch($uri, $payload = null, array $options = array()) 366 | { 367 | return $this->ini(Http::PATCH, $uri, $payload, $options); 368 | } 369 | 370 | /** 371 | * @param $uri 372 | * @param array $options 373 | * @return Request 374 | */ 375 | public function get($uri, array $options = array()) 376 | { 377 | return $this->ini(Http::GET, $uri, array(), $options); 378 | } 379 | 380 | 381 | /** 382 | * @param $uri 383 | * @param array $options 384 | * @param null $response 385 | * @return string 386 | */ 387 | public function quickGet($uri, array $options = array(), &$response = null) 388 | { 389 | $response = $this->get($uri, $options)->send(); 390 | return $response->body; 391 | } 392 | 393 | /** 394 | * @param $uri 395 | * @param array $options 396 | * @return Request 397 | */ 398 | function options($uri, array $options = array()) 399 | { 400 | return $this->ini(Http::OPTIONS, $uri, array(), $options); 401 | } 402 | 403 | /** 404 | * @param $uri 405 | * @param array $options 406 | * @return Request 407 | */ 408 | function head($uri, array $options = array()) 409 | { 410 | return $this->ini(Http::HEAD, $uri, array('CURLOPT_NOBODY' => true), $options); 411 | } 412 | 413 | /** 414 | * @param $uri 415 | * @param array $options 416 | * @return Request 417 | */ 418 | function delete($uri, array $options = array()) 419 | { 420 | return $this->ini(Http::DELETE, $uri, array(), $options); 421 | } 422 | 423 | /** 424 | * @param $uri 425 | * @param array $options 426 | * @return Request 427 | */ 428 | function trace($uri, array $options = array()) 429 | { 430 | return $this->ini(Http::TRACE, $uri, array(), $options); 431 | } 432 | 433 | /** 434 | * @param bool $isMultiCurl 435 | * @return Response 436 | */ 437 | public function send($isMultiCurl = false) 438 | { 439 | try { 440 | if (!$this->hasInitialized) 441 | $this->applyOptions(); 442 | $response = $this->makeResponse($isMultiCurl); 443 | $response->parse(); 444 | } catch (\Exception $e) { 445 | if(!isset($response)) $response = Response::create($this, null, null, null, null); 446 | $response->error = $e->getMessage(); 447 | $response->errorCode = 999; 448 | } 449 | 450 | if (self::$loggerHandler) { 451 | call_user_func(self::$loggerHandler, $response); 452 | } 453 | if ($this->endCallback) { 454 | call_user_func($this->endCallback, $response); 455 | } 456 | 457 | return $response; 458 | } 459 | 460 | /** 461 | * @return $this 462 | */ 463 | public function applyOptions() 464 | { 465 | $curl = curl_init(); 466 | $this->curlHandle = $curl; 467 | $this->prepare(); 468 | $this->hasInitialized = true; 469 | return $this; 470 | } 471 | 472 | /** 473 | * @return $this 474 | */ 475 | protected function prepare() 476 | { 477 | $this->options['url'] = trim($this->options['url']); 478 | if (empty($this->options['url'])) { 479 | throw new InvalidArgumentException('url can not empty'); 480 | } 481 | 482 | if (isset($this->options['retry_times'])) { 483 | $this->retryTimes = abs($this->options['retry_times']); 484 | } 485 | 486 | if (isset($this->options['retry_duration'])) { 487 | $this->retryDuration = abs($this->options['retry_duration']); 488 | } 489 | 490 | if(isset($this->options['expects_mime'])){ 491 | $this->expectsMime = $this->options['expects_mime']; 492 | } 493 | 494 | if(isset($this->options['send_mime'])){ 495 | $this->sendMime = $this->options['send_mime']; 496 | } 497 | 498 | // if(!empty($this->options['data']) && !Http::hasBody($this->options['method'])){ 499 | // $this->withURIQuery = is_array($this->options['data']) ? http_build_query($this->options['data']) : $this->options['data']; 500 | // } 501 | if (isset($this->withURIQuery)) { 502 | $this->options['url'] .= strpos($this->options['url'], '?') === FALSE ? '?' : '&'; 503 | $this->options['url'] .= $this->withURIQuery; 504 | } 505 | 506 | $this->serializeBody(); 507 | 508 | //try fix url 509 | if (strpos($this->options['url'], '://') === FALSE) $this->options['url'] = 'http://' . $this->options['url']; 510 | $components = parse_url($this->options['url']); 511 | if(FALSE === $components) throw new InvalidArgumentException('formatting url occurs error: '. $this->options['url']); 512 | if($this->withURIQuery){ 513 | if(isset($components['query'])) $components['query'] .= '&'. trim($this->withURIQuery); 514 | else $components['query'] = trim($this->withURIQuery); 515 | } 516 | $this->options['url'] = self::combineUrl($components); 517 | 518 | if (isset($this->options['callback'])) { 519 | $this->onEnd($this->options['callback']); 520 | } 521 | //swap ip and host 522 | if (!empty($this->options['ip'])) { 523 | $matches = array(); 524 | preg_match('/\/\/([^\/]+)/', $this->options['url'], $matches); 525 | $host = $matches[1]; 526 | if (empty($this->options['headers']) || !is_array($this->options['headers'])) { 527 | $this->options['headers'] = array('Host: ' . $host); 528 | } else { 529 | $this->options['headers'][] = 'Host: ' . $host; 530 | } 531 | $this->options['url'] = str_replace("//{$host}", '//' . $this->options['ip'], $this->options['url']); 532 | unset($host); 533 | } 534 | //process version 535 | if (!empty($this->options['http_version'])) { 536 | $version = $this->options['http_version']; 537 | if ($version == '1.0') { 538 | $this->options['CURLOPT_HTTP_VERSION'] = CURLOPT_HTTP_VERSION_1_0; 539 | } elseif ($version == '1.1') { 540 | $this->options['CURLOPT_HTTP_VERSION'] = CURLOPT_HTTP_VERSION_1_1; 541 | } 542 | 543 | unset($version); 544 | } 545 | 546 | //convert secs to milliseconds 547 | if (defined('CURLOPT_TIMEOUT_MS')) { 548 | if (!isset($this->options['timeout_ms'])) { 549 | $this->options['timeout_ms'] = intval($this->options['timeout'] * 1000); 550 | } else { 551 | $this->options['timeout_ms'] = intval($this->options['timeout_ms']); 552 | } 553 | } 554 | 555 | $cURLOptions = self::filterAndRaw($this->options); 556 | if(isset($this->body))$cURLOptions[CURLOPT_POSTFIELDS] = $this->body;//use serialized body not raw data 557 | curl_setopt_array($this->curlHandle, $cURLOptions); 558 | 559 | return $this; 560 | } 561 | 562 | public function serializeBody() 563 | { 564 | //Passing an array to CURLOPT_POSTFIELDS will encode the data as multipart/form-data, while passing a URL-encoded string will encode the data as application/x-www-form-urlencoded. 565 | if (isset($this->options['data'])) { 566 | $this->options[CURLOPT_POST] = true; 567 | $clz = '\\MultiHttp\\Handler\\'.ucfirst($this->sendMime); 568 | $inst = new $clz; 569 | if (!($inst instanceof Handler\IHandler)) throw new InvalidOperationException($clz . ' is not implement of IHandler'); 570 | $this->body = $inst->encode($this->options['data']); 571 | } 572 | } 573 | 574 | /** 575 | * @param callable $callback 576 | * @return $this 577 | */ 578 | public function onEnd(callable $callback) 579 | { 580 | if (!is_callable($callback)) { 581 | throw new InvalidArgumentException('callback not is callable :' . print_r($callback, 1)); 582 | } 583 | 584 | $this->endCallback = $callback; 585 | return $this; 586 | } 587 | 588 | /** 589 | * @param array $options 590 | * @return array 591 | */ 592 | protected static function filterAndRaw(array &$options) 593 | { 594 | $opts = $fullsOpts = array(); 595 | foreach ($options as $key => $val) { 596 | $fullOption = self::fullOption($key); 597 | 598 | if ($fullOption) { 599 | $fullsOpts[$fullOption] = $val; 600 | $opts[constant($fullOption)] = $val; 601 | } 602 | unset($options[$key]); 603 | } 604 | $options = $fullsOpts; 605 | return $opts; 606 | } 607 | 608 | /** 609 | * @param bool $isMultiCurl 610 | * @return Response 611 | * @throws \Exception 612 | */ 613 | public function makeResponse($isMultiCurl = false) 614 | { 615 | $handle = $this->curlHandle; 616 | $body = $errno = null; 617 | Helper::retry($this->retryTimes, function()use(&$body, &$errno, $isMultiCurl, $handle){ 618 | $body = $isMultiCurl ? curl_multi_getcontent($handle) : curl_exec($handle); 619 | $errno = curl_errno($handle); 620 | return 0 == $errno; 621 | }, $this->retryDuration); 622 | 623 | $info = curl_getinfo($this->curlHandle); 624 | $errorCode = curl_errno($this->curlHandle); 625 | $error = curl_error($this->curlHandle); 626 | $response = Response::create($this, $body, $info, $errorCode, $error); 627 | return $response; 628 | } 629 | 630 | 631 | } 632 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | request = $request; 59 | $self->body = $body; 60 | $self->info = $info; 61 | $self->errorCode = $errorCode; 62 | $self->error = $error; 63 | return $self; 64 | } 65 | 66 | 67 | 68 | public function parse() 69 | { 70 | //has header 71 | $headers = rtrim(substr($this->body, 0, $this->info['header_size'])); 72 | $this->body = substr($this->body, $this->info['header_size']); 73 | $headers = explode(PHP_EOL, $headers); 74 | array_shift($headers); // HTTP HEADER 75 | foreach ($headers as $h) { 76 | if (false !== strpos($h, ':')) 77 | list($k, $v) = explode(':', $h, 2); 78 | else 79 | list($k, $v) = array($h, ''); 80 | 81 | $this->header[trim($k)] = trim($v); 82 | } 83 | 84 | $this->code = $this->info['http_code']; 85 | $this->duration = $this->info['total_time']; 86 | $this->contentType = $this->info['content_type']; 87 | $content_type = isset($this->info['content_type']) ? $this->info['content_type'] : ''; 88 | $content_type = explode(';', $content_type); 89 | $this->contentType = $content_type[0]; 90 | if (count($content_type) == 2 && strpos($content_type[1], '=') !== false) { 91 | list(, $this->charset) = explode('=', $content_type[1]); 92 | } 93 | 94 | $this->unserializeBody(); 95 | 96 | } 97 | public function unserializeBody() 98 | { 99 | if (isset($this->request->expectsMime)) { 100 | if (Mime::getFullMime($this->request->expectsMime) !== $this->contentType) throw new UnexpectedResponseException('expected mime can not be matched, real mime:'. $this->contentType. ', expected mime:'. Mime::getFullMime($this->request->expectsMime)); 101 | $clz = "\MultiHttp\\Handler\\".ucfirst($this->request->expectsMime); 102 | $inst = new $clz; 103 | if (!($inst instanceof Handler\IHandler)) throw new InvalidOperationException($clz . ' is not implement of IHandler'); 104 | $this->body = $inst->decode($this->body); 105 | } 106 | } 107 | /** 108 | * Status Code Definitions 109 | * 110 | * Informational 1xx 111 | * Successful 2xx 112 | * Redirection 3xx 113 | * Client Error 4xx 114 | * Server Error 5xx 115 | * 116 | * http://pretty-rfc.herokuapp.com/RFC2616#status.codes 117 | * 118 | * @return bool Did we receive a 4xx or 5xx? 119 | */ 120 | public function hasErrors() 121 | { 122 | return $this->code == 0 || $this->code >= 400; 123 | } 124 | 125 | } -------------------------------------------------------------------------------- /test.md: -------------------------------------------------------------------------------- 1 | composer install 2 | composer dumpautoload 3 | phpunit -------------------------------------------------------------------------------- /tests/MultiRequestTest.php: -------------------------------------------------------------------------------- 1 | setDefaults(array( 30 | 'timeout' => 2 31 | ))->addOptions( 32 | array( 33 | array( 34 | 'url' => TEST_SERVER . '/dynamic/all_data.php?&a=1', 35 | 'method' => 'HEAD', 36 | 'data' => array(), 37 | 'callback' => function (Response $response) { 38 | //test setDefaults 39 | self::assertEquals(2, $response->request->getIni('timeout')); 40 | self::assertEmpty($response->body); 41 | 42 | self::assertTrue(is_array($response->header) && sizeof($response->header)>0); 43 | self::assertLessThan(1.5, $response->duration); 44 | self::assertFalse($response->hasErrors(), $response->request->uri . $response->error); 45 | self::assertEquals(TEST_SERVER . '/dynamic/all_data.php?&a=1', $response->request->uri); 46 | self::assertEquals(Request::HEAD, $response->request->getIni('method')); 47 | self::assertTrue($response->request->hasEndCallback()); 48 | } 49 | ), 50 | 51 | array( 52 | 'url' => TEST_SERVER . '/dynamic/all_data.php?&a&data=this_is_post_data', 53 | 'data' => array( 54 | ), 55 | 'expects_mime' => 'json', 56 | 'callback' => function (Response $response) { 57 | //test json 58 | self::assertNotEmpty($response->body); 59 | //test setDefaults 60 | self::assertEquals(2, $response->request->getIni('timeout')); 61 | self::assertFalse($response->hasErrors(), $response->request->uri . $response->error); 62 | self::assertEquals(Request::GET, $response->request->getIni('method')); 63 | self::assertTrue($response->request->hasEndCallback()); 64 | self::assertContains('this_is_post_data', $response->body['g'] ); 65 | } 66 | ), 67 | array( 68 | 'url' => TEST_SERVER . '/dynamic/all_data.php?&b', 69 | 'method' => 'POST', 70 | 'expects_mime' => 'json', 71 | 'data' => array( 72 | 'data' => 'this_is_post_data', 73 | ), 74 | 'callback' => function (Response $response) { 75 | //test json 76 | self::assertNotEmpty($response->body); 77 | //test setDefaults 78 | self::assertEquals(2, $response->request->getIni('timeout')); 79 | 80 | self::assertFalse($response->hasErrors(), $response->request->uri . $response->error); 81 | self::assertEquals(TEST_SERVER . '/dynamic/all_data.php?&b', $response->request->uri); 82 | self::assertEquals(Request::POST, $response->request->getIni('method')); 83 | self::assertTrue($response->request->hasEndCallback()); 84 | self::assertContains('this_is_post_data', $response->body['p']['data']); 85 | } 86 | ), 87 | array( 88 | 'url' => TEST_SERVER . '/static/test.json', 89 | 'callback' => function (Response $response) { 90 | self::assertFalse($response->hasErrors(), $response->request->uri . $response->error); 91 | self::assertEquals(TEST_SERVER . '/static/test.json', $response->request->uri); 92 | self::assertNotEmpty($response->body); 93 | self::assertJsonStringEqualsJsonFile(WEB_SERVER_DOCROOT . '/static/test.json', $response->body); 94 | } 95 | ), 96 | array( 97 | 'url' => 'http://www.qq.com', 98 | 'timeout' => 3, 99 | 'callback' => function (Response $response) { 100 | //test json 101 | self::assertEquals(true, strlen($response->body)>0); 102 | //test setDefaults 103 | self::assertEquals(3, $response->request->getIni('timeout')); 104 | 105 | self::assertContains('http://www.qq.com', $response->request->uri); 106 | self::assertTrue($response->request->hasEndCallback()); 107 | }, 108 | ), 109 | array( 110 | 'url' => 'http://proxy.test/dynamic/all_data.php', 111 | 'ip' => WEB_SERVER_HOST , 112 | 'port' => WEB_SERVER_PORT, 113 | 'timeout' => 0,//unlimited timeout 114 | 'callback' => function (Response $response) { 115 | self::assertFalse($response->hasErrors()); 116 | self::assertNotEmpty($response->body); 117 | } 118 | ), 119 | array( 120 | 'url' => TEST_SERVER.'/dynamic/all_data.php', 121 | 'expects_mime' => 'json', 122 | 'send_mime' => 'json', 123 | 'method' => 'POST', 124 | 'data' => array('aaa'=>'bbc2'), 125 | 'timeout' => 0,//unlimited timeout 126 | 'callback' => function (Response $response) { 127 | self::assertFalse($response->hasErrors()); 128 | self::assertEquals(array('aaa'=>'bbc2'), $response->request->getIni('data')); 129 | self::assertEquals('{"aaa":"bbc2"}', $response->body['postRaw']); 130 | self::assertEquals(array(), $response->body['p']); 131 | self::assertTrue(is_array($response->body) && sizeof($response->body)); 132 | } 133 | ), 134 | array( 135 | 'url' => TEST_SERVER.'/dynamic/all_data.php', 136 | 'method' => 'POST', 137 | 'data' => array('aaa'=>'bbc'), 138 | 'timeout' => 0,//unlimited timeout 139 | 'callback' => function (Response $response) { 140 | self::assertFalse($response->hasErrors()); 141 | self::assertTrue(is_string($response->body) && strlen($response->body)); 142 | } 143 | ), 144 | )) 145 | ->add(Http::GET, 'http://www.163.com', array(), array( 146 | 'timeout' => 3, 147 | 'callback' => function (Response $response) { 148 | self::assertContains('http://www.163.com', $response->request->uri); 149 | }, 150 | )) 151 | ->add('GET', 'http://sina.cn', array(), array( 152 | 'timeout' => 3, 153 | )) 154 | ->import(Request::create()->trace('http://sohu.cn', array( 155 | 'timeout' => 3, 156 | 'callback' => function (Response $response) { 157 | self::assertContains('http://sohu.cn', $response->request->uri); 158 | }))->applyOptions()) 159 | ->import(Request::create()->options('http://toutiao.com', array( 160 | 'timeout' => 3, 161 | 'callback' => function (Response $response) { 162 | self::assertContains('http://toutiao.com', $response->request->uri); 163 | }))->applyOptions()) 164 | ->sendAll(); 165 | echo "exec done\n\t"; 166 | foreach ($rtn as $response) { 167 | echo $response->request->uri, ' takes:', $response->duration, ' ', "\n\t\n\t"; 168 | } 169 | $end = microtime(1); 170 | echo 'multi total takes:', $end - $start, ' secs;'; 171 | self::assertTrue($end - $start < 5); 172 | } 173 | 174 | protected function setUp() 175 | { 176 | parent::setUp(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/RequestTest.php: -------------------------------------------------------------------------------- 1 | quickGet('https://github.com/',[ 26 | 'callback' => function($response){ 27 | $this->assertNotEmpty($response->body); 28 | } 29 | ]); 30 | } 31 | function testQuick() 32 | { 33 | $response = null; 34 | $uniqId = uniqid(); 35 | \MultiHttp\Request::create()->quickGet(TEST_SERVER . '/dynamic/all_data.php' . '?aa=' . $uniqId, [ 36 | 'callback' => function (\MultiHttp\Response $response) use ($uniqId) { 37 | $this->assertInstanceOf('MultiHttp\Response', $response); 38 | $result = json_decode($response->body,1); 39 | $this->assertEquals($uniqId, $result['g']['aa'], "assert error \n"); 40 | } 41 | ], $response); 42 | $uniqId = uniqid(); 43 | \MultiHttp\Request::create()->quickPost(TEST_SERVER . '/dynamic/all_data.php?aa=AA', 44 | [ 45 | 'bb' => $uniqId 46 | ] 47 | , [ 48 | 'callback' => function (\MultiHttp\Response $response) use ($uniqId) { 49 | $this->assertInstanceOf('MultiHttp\Response', $response); 50 | $result = json_decode($response->body,1); 51 | $this->assertEquals('AA', $result['g']['aa'], "assert error \n"); 52 | $this->assertEquals($uniqId, $result['p']['bb'], "assert error \n"); 53 | } 54 | ], $response); 55 | $this->assertInstanceOf('MultiHttp\Response', $response); 56 | 57 | \MultiHttp\Request::create()->quickGet('http://google.com', [ 58 | 'timeout' => 1, 59 | 'retry_times' => 3, 60 | 'retry_duration' => 1, 61 | 'callback' => function ($response) { 62 | $this->assertInstanceOf('MultiHttp\Response', $response); 63 | } 64 | ], $response); 65 | $this->assertInstanceOf('MultiHttp\Response', $response); 66 | 67 | echo "\n\n\n"; 68 | \MultiHttp\Request::create()->quickPost('http://facebook.com', [], [ 69 | 'timeout' => 3, 70 | 'retry_times' => 2, 71 | 'retry_duration' => 1, 72 | 'callback' => function ($response) { 73 | $this->assertInstanceOf('MultiHttp\Response', $response); 74 | } 75 | ], $response); 76 | $this->assertInstanceOf('MultiHttp\Response', $response); 77 | } 78 | 79 | function testLazy() 80 | { 81 | $mr = \MultiHttp\MultiRequest::create(); 82 | $mr->add('GET', 'http://sina.cn', array(), array( 83 | 'timeout' => 3, 84 | 'callback'=> function($response){ 85 | $this->assertInstanceOf('MultiHttp\Response', $response); 86 | $this->assertNotEmpty($response->body); 87 | //do sth with response 88 | } 89 | )); 90 | $mr->add('POST', TEST_SERVER.'/dynamic/all_data.php?sleep=1', array(), array( 91 | 'timeout' => 3, 92 | 'callback'=> function($response){ 93 | $this->assertInstanceOf('MultiHttp\Response', $response); 94 | $this->assertNotEmpty($response->body); 95 | //do sth with response 96 | } 97 | )); 98 | //add ... 99 | // $mr->sendAll(); 100 | $results = $mr->sendAll(); 101 | echo 'size of results : '. sizeof($results)."\n"; 102 | } 103 | 104 | function testRetry() 105 | { 106 | $i = 0; 107 | $trys = \MultiHttp\Helper::retry(5, function () use (&$i) { 108 | if (++$i > 3) { 109 | echo $i . "\n"; 110 | ob_flush(); 111 | flush(); 112 | return true; 113 | } else { 114 | echo $i . "\n"; 115 | ob_flush(); 116 | flush(); 117 | return false; 118 | } 119 | }, 1); 120 | echo 'retry times: ' . $trys . "\n"; 121 | } 122 | 123 | 124 | function test() 125 | { 126 | $start = microtime(1); 127 | $responses = array(); 128 | Request::setLogHandler(function(Response $response){ 129 | echo PHP_EOL.'LogHandler: '. $response->request->uri . " info: " . $response->error. PHP_EOL; 130 | }); 131 | //test https get 132 | $responses[] = Request::create()->get('https://www.baidu.com/?q=cms&a=b#c=d', array( 133 | 'timeout' => 5, 134 | 'callback' => function (Response $response) { 135 | self::assertTrue(strlen($response->body) > 0); 136 | } 137 | ))->addQuery('e=f')->send(); 138 | 139 | //test timeout/http get 140 | $responses[] = Request::create()->addQuery('sleep=2')->get(TEST_SERVER . '/dynamic/all_data.php', array( 141 | 'timeout' => 3, 142 | 'timeout_ms' => 2000, 143 | 'callback' => function (Response $response) { 144 | self::assertLessThan(3, $response->duration); 145 | self::assertGreaterThan(1, $response->duration); 146 | self::assertTrue($response->hasErrors(), $response->error); 147 | self::assertTrue(!$response->body); 148 | self::assertEquals(TEST_SERVER . '/dynamic/all_data.php', $response->request->uri); 149 | self::assertEquals(true,$response->request->hasEndCallback()); 150 | }))->send(); 151 | //test expectsJson/headers 152 | $responses[] = Request::create()->addQuery('sleep=2')->get(TEST_SERVER . '/dynamic/all_data.php#1', array( 153 | 'timeout' => 3, 154 | 'callback' => function (Response $response) { 155 | self::assertLessThan(3, $response->duration); 156 | self::assertGreaterThan(2, $response->duration); 157 | self::assertTrue(!$response->hasErrors(), $response->error); 158 | self::assertTrue(is_array($response->body)); 159 | self::assertTrue(sizeof($response->body)>0); 160 | self::assertEquals(TEST_SERVER . '/dynamic/all_data.php#1', $response->request->uri); 161 | self::assertEquals(true,$response->request->hasEndCallback()); 162 | }))->sendMime('json')->expectsMime('json')->addHeader('Debug', 'addHeader')->addHeaders(array('Debug2'=> 'addHeaders哈"'))->send(); 163 | 164 | //test trace 165 | $responses[] = Request::create()->addQuery(array('sleep' => 2))->trace(TEST_SERVER . '/dynamic/all_data.php', array( 166 | 'timeout' => 3, 167 | 'callback' => function (Response $response) { 168 | self::assertTrue(strlen($response->body) > 0); 169 | self::assertFalse($response->hasErrors()); 170 | self::assertJson($response->body); 171 | self::assertEquals(TEST_SERVER . '/dynamic/all_data.php', $response->request->uri); 172 | self::assertEquals(3, $response->request->getIni('timeout')); 173 | }))->send(); 174 | //test put 175 | $responses[] = Request::create()->put(TEST_SERVER . '/static/test.json')->onEnd(function (Response $response) { 176 | self::assertFalse($response->hasErrors()); 177 | self::assertEquals(TEST_SERVER . '/static/test.json', $response->request->uri); 178 | self::assertTrue(strlen($response->body) > 0); 179 | self::assertJsonStringEqualsJsonFile(WEB_SERVER_DOCROOT . '/static/test.json', $response->body); 180 | })->send(); 181 | 182 | //test patch/addOptions 183 | $responses[] = Request::create()->get(TEST_SERVER . '/dynamic/all_data.php', array( 184 | 'callback' => function (Response $response) { 185 | self::assertEquals(Request::PATCH, $response->request->getIni('method')); 186 | self::assertTrue($response->request->hasEndCallback()); 187 | }))->addOptions(array( 188 | 'method' => Request::PATCH 189 | ))->send(); 190 | $responses[] = Request::create()->patch(TEST_SERVER . '/dynamic/all_data.php', array( 191 | 'callback' => function (Response $response) { 192 | self::assertEquals(Request::PATCH, $response->request->getIni('method')); 193 | self::assertTrue($response->request->hasEndCallback()); 194 | }))->send(); 195 | 196 | //test post 197 | $responses[] = Request::create()->post(TEST_SERVER . '/dynamic/all_data.php', array('data' => 'this_is_post_data'), array( 198 | 'callback' => function (Response $response) { 199 | self::assertEquals(Request::POST, $response->request->getIni('method')); 200 | self::assertTrue($response->request->hasEndCallback()); 201 | self::assertContains('this_is_post_data', $response->body); 202 | 203 | }))->send(); 204 | 205 | //test delete 206 | $responses[] = Request::create()->delete(TEST_SERVER . '/dynamic/all_data.php', array(), array( 207 | // 'data' => 'data=this_is_post_data', //not work 208 | 'callback' => function (Response $response) { 209 | self::assertEquals(Request::DELETE, $response->request->getIni('method')); 210 | self::assertTrue($response->request->hasEndCallback()); 211 | self::assertNotContains('this_is_post_data', $response->body); 212 | 213 | }))->send(); 214 | 215 | //test proxy ip 216 | $responses[] = Request::create()->get('http://test-proxy.local/dynamic/all_data.php', array( 217 | 'ip' => WEB_SERVER_HOST, 218 | 'port' => WEB_SERVER_PORT, 219 | 'timeout' => 2, 220 | 'callback' => function (Response $response) { 221 | self::assertFalse($response->hasErrors()); 222 | self::assertTrue(strlen($response->body) > 0); 223 | } 224 | ))->send(); 225 | 226 | //test head 227 | $response = Request::create()->head(TEST_SERVER . '/dynamic/all_data.php?head', array( 228 | 'callback' => function (Response $response) { 229 | self::assertEmpty($response->body); 230 | } 231 | ))->applyOptions()->send(); 232 | self::assertInstanceOf('\MultiHttp\Response', $response); 233 | self::assertEmpty($response->body); 234 | self::assertNotEmpty($response->header); 235 | 236 | echo "\n\t\n\texec done\n\t\n\t"; 237 | foreach ($responses as $response) { 238 | echo $response->request->uri, ' takes:', $response->duration, "\n\t\n\t"; 239 | } 240 | $end = microtime(1); 241 | echo 'total takes:', $end - $start, ' secs;'; 242 | 243 | } 244 | function testForm(){ 245 | $result = \MultiHttp\Request::create()->quickPost(TEST_SERVER . '/dynamic/all_data.php', [ 246 | 'field_a' => 'post_data', 247 | 'file_a' => "@".__DIR__."/static/test_image.jpg", 248 | ]); 249 | $result = json_decode($result, 1); 250 | $this->assertEquals('post_data', $result['p']['field_a']); 251 | $this->assertNotEmpty($result['p']['file_a']); 252 | } 253 | function testUpload(){ 254 | $result = \MultiHttp\Request::create()->upload(TEST_SERVER . '/dynamic/all_data.php', [ 255 | 'field_a' => 'post_data', 256 | 'file_a' => "@".__DIR__."/static/test_image.jpg", 257 | // 'file_a' => new \CURLFile(__DIR__."/static/test_image.jpg"), 258 | ])->send(); 259 | $result = json_decode($result->body, 1); 260 | $this->assertEquals('post_data', $result['p']['field_a']); 261 | $this->assertNotEmpty($result['f']['file_a']['size']); 262 | } 263 | 264 | } 265 | -------------------------------------------------------------------------------- /tests/bootstrap-server.php: -------------------------------------------------------------------------------- 1 | ./server.log 2>&1 & echo $!', WEB_SERVER_HOST, WEB_SERVER_PORT, WEB_SERVER_DOCROOT); 19 | 20 | // Execute the command and store the process ID 21 | $output = array(); 22 | exec($command, $output, $exit_code); 23 | 24 | // sleep for a second to let server come up 25 | sleep(1); 26 | $pid = (int) $output[0]; 27 | 28 | // check server.log to see if it failed to start 29 | $server_logs = file_get_contents("./server.log"); 30 | if (strpos($server_logs, "Fail") !== false) { 31 | // server failed to start for some reason 32 | print "Failed to start server! Logs:" . PHP_EOL . PHP_EOL; 33 | print_r($server_logs); 34 | exit(1); 35 | } 36 | 37 | echo sprintf('%s - Web server started on %s:%d with PID %d', date('r'), WEB_SERVER_HOST, WEB_SERVER_PORT, $pid) . PHP_EOL; 38 | 39 | register_shutdown_function(function() { 40 | // cleanup after ourselves -- remove log file, shut down server 41 | global $pid; 42 | unlink("./server.log"); 43 | posix_kill($pid, SIGKILL); 44 | echo sprintf('%s - Web server terminal on %s:%d with PID %d', date('r'), WEB_SERVER_HOST, WEB_SERVER_PORT, $pid) . PHP_EOL; 45 | 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | $sleep, 18 | 'f' => $_FILES, 19 | 'g' => $_GET, 20 | 'p' => $_POST, 21 | 's' => $_SERVER, 22 | 'postRaw' => file_get_contents("php://input"), 23 | ]); 24 | -------------------------------------------------------------------------------- /tests/static/test.json: -------------------------------------------------------------------------------- 1 | {"foo": "bar", "baz": false, "float":9.9} 2 | -------------------------------------------------------------------------------- /tests/static/test_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinacms/MultiHttp/0f245757c9fa5a6f2e4ac9a6e6e04492a8df756e/tests/static/test_image.jpg -------------------------------------------------------------------------------- /usage.md: -------------------------------------------------------------------------------- 1 | ## Table of contents 2 | 3 | - [\MultiHttp\Http (abstract)](#class-multihttphttp-abstract) 4 | - [\MultiHttp\Mime](#class-multihttpmime) 5 | - [\MultiHttp\MultiRequest](#class-multihttpmultirequest) 6 | - [\MultiHttp\Request](#class-multihttprequest) 7 | - [\MultiHttp\Response](#class-multihttpresponse) 8 | - [\MultiHttp\Exception\InvalidArgumentException](#class-multihttpexceptioninvalidargumentexception) 9 | - [\MultiHttp\Exception\InvalidOperationException](#class-multihttpexceptioninvalidoperationexception) 10 | - [\MultiHttp\Exception\UnexpectedResponseException](#class-multihttpexceptionunexpectedresponseexception) 11 | 12 |
13 | 14 | ### Class: \MultiHttp\Http (abstract) 15 | 16 | > Class Http 17 | 18 | | Visibility | Function | 19 | |:-----------|:---------| 20 | | public | abstract delete(mixed $uri, array $options=array()) : mixed | 21 | | public | abstract get(mixed $uri, array $options=array()) : mixed | 22 | | public | abstract head(mixed $uri, array $options=array()) : mixed | 23 | | public | abstract options(mixed $uri, array $options=array()) : mixed | 24 | | public | abstract patch(mixed $uri, null $payload=null, array $options=array()) : mixed | 25 | | public | abstract post(mixed $uri, null $payload=null, array $options=array()) : mixed | 26 | | public | abstract put(mixed $uri, null $payload=null, array $options=array()) : mixed | 27 | | public | abstract trace(mixed $uri, array $options=array()) : mixed | 28 | 29 |
30 | 31 | ### Class: \MultiHttp\Mime 32 | 33 | > Class to organize the Mime stuff a bit more 34 | 35 | | Visibility | Function | 36 | |:-----------|:---------| 37 | | public static | getFullMime(string $short_name) : string full mime type (e.g. application/json)
Get the full Mime Type name from a "short name". Returns the short if no mapping was found. | 38 | | public static | supportsMimeType(string $short_name) : bool | 39 | 40 |
41 | 42 | ### Class: \MultiHttp\MultiRequest 43 | 44 | | Visibility | Function | 45 | |:-----------|:---------| 46 | | public | add(mixed $method, mixed $uri, mixed $payload, array $options=array()) : \MultiHttp\$this | 47 | | public | addOptions(array $URLOptions) : \MultiHttp\$this
example: array(array('url'=>'http://localhost:9999/','timeout'=>1, 'method'=>'POST', 'data'=>'aa=bb&c=d')) | 48 | | public static | create() : [\MultiHttp\MultiRequest](#class-multihttpmultirequest) | 49 | | public | import([\MultiHttp\Request](#class-multihttprequest) $request) : \MultiHttp\$this | 50 | | public | sendAll() : \MultiHttp\array(Response) | 51 | | public | setDefaults(array $options=array()) : \MultiHttp\$this | 52 | | protected | __construct() : void
MultiRequest constructor. | 53 | | protected static | prepare() : void | 54 | 55 |
56 | 57 | ### Class: \MultiHttp\Request 58 | 59 | > Class Request 60 | 61 | | Visibility | Function | 62 | |:-----------|:---------| 63 | | public | addHeader(mixed $headerName, mixed $value) : \MultiHttp\$this | 64 | | public | addHeaders(array $headers) : \MultiHttp\$this | 65 | | public | addOptions(array $options=array()) : \MultiHttp\$this | 66 | | public | addQuery(mixed $data) : \MultiHttp\$this | 67 | | public | applyOptions() : \MultiHttp\$this | 68 | | public static | create() : [\MultiHttp\Request](#class-multihttprequest) | 69 | | public | delete(mixed $uri, array $options=array()) : [\MultiHttp\Request](#class-multihttprequest) | 70 | | public | endCallback() : mixed | 71 | | public | expectsMime(string $mime=`'json'`) : \MultiHttp\$this | 72 | | public | get(mixed $uri, array $options=array()) : [\MultiHttp\Request](#class-multihttprequest) | 73 | | public | getIni(mixed $field=null) : bool/mixed | 74 | | public | hasEndCallback() : bool | 75 | | public | head(mixed $uri, array $options=array()) : [\MultiHttp\Request](#class-multihttprequest) | 76 | | public | json(mixed $body) : string | 77 | | public | makeResponse(bool $isMultiCurl=false) : [\MultiHttp\Response](#class-multihttpresponse) | 78 | | public | onEnd(\callable $callback) : \MultiHttp\$this | 79 | | public | options(mixed $uri, array $options=array()) : [\MultiHttp\Request](#class-multihttprequest) | 80 | | public | patch(mixed $uri, null $payload=null, array $options=array()) : [\MultiHttp\Request](#class-multihttprequest) | 81 | | public | post(mixed $uri, null $payload=null, array $options=array()) : [\MultiHttp\Request](#class-multihttprequest) | 82 | | public | put(mixed $uri, null $payload=null, array $options=array()) : [\MultiHttp\Request](#class-multihttprequest) | 83 | | public | send(bool $isMultiCurl=false) : [\MultiHttp\Response](#class-multihttpresponse) | 84 | | public | sendMime(string $mime=`'json'`) : [\MultiHttp\Request](#class-multihttprequest) | 85 | | public | serializeBody() : void | 86 | | public static | setLogHandler(\callable $handler) : void | 87 | | public | timeout(mixed $timeout) : \MultiHttp\$this | 88 | | public | trace(mixed $uri, array $options=array()) : [\MultiHttp\Request](#class-multihttprequest) | 89 | | public | unJson(mixed $body) : mixed | 90 | | public | uri(mixed $uri) : \MultiHttp\$this | 91 | | protected | __construct() : void
Request constructor. | 92 | | protected static | filterAndRaw(array $options) : array | 93 | | protected static | fullOption(mixed $key) : mixed | 94 | | protected | ini(mixed $method, mixed $url, mixed $data, array $options=array()) : \MultiHttp\$this | 95 | | protected | prepare() : \MultiHttp\$this | 96 | 97 | *This class extends [\MultiHttp\Http](#class-multihttphttp-abstract)* 98 | 99 |
100 | 101 | ### Class: \MultiHttp\Response 102 | 103 | > Class Response 104 | 105 | | Visibility | Function | 106 | |:-----------|:---------| 107 | | public static | create([\MultiHttp\Request](#class-multihttprequest) $request, mixed $body, mixed $info, mixed $errorCode, mixed $error) : [\MultiHttp\Response](#class-multihttpresponse) | 108 | | public | hasErrors() : bool Did we receive a 4xx or 5xx?
Status Code Definitions Informational 1xx Successful 2xx Redirection 3xx Client Error 4xx Server Error 5xx http://pretty-rfc.herokuapp.com/RFC2616#status.codes | 109 | | public | parse() : void | 110 | | public | unserializeBody() : void | 111 | | protected | __construct() : void
Response constructor. | 112 | 113 |
114 | 115 | ### Class: \MultiHttp\Exception\InvalidArgumentException 116 | 117 | > Class InvalidArgumentException 118 | 119 | | Visibility | Function | 120 | |:-----------|:---------| 121 | 122 | *This class extends \LogicException* 123 | 124 | *This class implements \Throwable* 125 | 126 |
127 | 128 | ### Class: \MultiHttp\Exception\InvalidOperationException 129 | 130 | > Class InvalidOperationException 131 | 132 | | Visibility | Function | 133 | |:-----------|:---------| 134 | 135 | *This class extends \LogicException* 136 | 137 | *This class implements \Throwable* 138 | 139 |
140 | 141 | ### Class: \MultiHttp\Exception\UnexpectedResponseException 142 | 143 | > Class UnexpectedResponseException 144 | 145 | | Visibility | Function | 146 | |:-----------|:---------| 147 | 148 | *This class extends \UnexpectedValueException* 149 | 150 | *This class implements \Throwable* 151 | 152 | --------------------------------------------------------------------------------