├── .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://travis-ci.org/sinacms/MultiHttp)
4 | [](https://scrutinizer-ci.com/g/sinacms/MultiHttp)
5 | [](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 | 
19 | 
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 |
--------------------------------------------------------------------------------