├── src ├── ConnectionException.php ├── Facade.php ├── RequestException.php ├── Retry.php ├── Http.php ├── Response.php ├── Logger.php └── Request.php ├── composer.json ├── LICENSE └── README.md /src/ConnectionException.php: -------------------------------------------------------------------------------- 1 | facade = new $this->facade; 12 | } 13 | 14 | public function __call($name, $params) 15 | { 16 | if (method_exists($this->facade, 'removeOptions')) { 17 | call_user_func_array([$this->facade, 'removeOptions'], []); 18 | } 19 | return call_user_func_array([$this->facade, $name], $params); 20 | } 21 | 22 | public static function __callStatic($name, $params) 23 | { 24 | return call_user_func_array([new static(), $name], $params); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yzh52521/easyhttp", 3 | "description": "EasyHttp 是一个轻量级、语义化、对IDE友好的HTTP客户端,支持常见的HTTP请求、异步请求和并发请求,让你可以快速地使用 HTTP 请求与其他 Web 应用进行通信。", 4 | "license": "MIT", 5 | "keywords": [ 6 | "easyhttp", 7 | "EasyHttp", 8 | "php-http", 9 | "phphttp", 10 | "easy-http", 11 | "php", 12 | "http", 13 | "curl" 14 | ], 15 | "homepage": "https://github.com/yzh52521/easyhttp", 16 | "authors": [ 17 | { 18 | "name": "yzh52521", 19 | "email": "396751927@qq.com" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=7.2.5", 24 | "guzzlehttp/guzzle": "^6.0|^7.0", 25 | "psr/log":"^1.0|^2.0|^3.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "yzh52521\\EasyHttp\\": "src/" 30 | } 31 | }, 32 | "minimum-stability": "stable" 33 | } 34 | -------------------------------------------------------------------------------- /src/RequestException.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 12 | } 13 | 14 | public function getCode() 15 | { 16 | return $this->exception->getCode(); 17 | } 18 | 19 | public function getMessage() 20 | { 21 | return $this->exception->getMessage(); 22 | } 23 | 24 | public function getFile() 25 | { 26 | return $this->exception->getFile(); 27 | } 28 | 29 | public function getLine() 30 | { 31 | return $this->exception->getLine(); 32 | } 33 | 34 | public function getTrace() 35 | { 36 | return $this->exception->getTrace(); 37 | } 38 | 39 | public function getTraceAsString() 40 | { 41 | return $this->exception->getTraceAsString(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present rap2hpoutre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Retry.php: -------------------------------------------------------------------------------- 1 | decider($retries), $this->delay($sleep)); 17 | } 18 | 19 | protected function decider(int $times) 20 | { 21 | return function ( 22 | $retries, 23 | Request $request, 24 | Response $response = null, 25 | RequestException $exception = null 26 | ) use ($times) { 27 | // 超过最大重试次数,不再重试 28 | if ($retries >= $times) { 29 | return false; 30 | } 31 | return $exception instanceof ConnectException || $exception instanceof ServerException || ($response && $response->getStatusCode() >= 500); 32 | }; 33 | } 34 | 35 | /** 36 | * 返回一个匿名函数,该匿名函数返回下次重试的时间(毫秒) 37 | * @param int $retry_delay 38 | * @return \Closure 39 | */ 40 | protected function delay(int $retry_delay) 41 | { 42 | return function ($retries) use ($retry_delay) { 43 | return $retry_delay * $retries; 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Http.php: -------------------------------------------------------------------------------- 1 | response = $response; 22 | } 23 | 24 | /** 25 | * Get the body of the response. 26 | * @return string 27 | */ 28 | public function body() 29 | { 30 | return (string)$this->response->getBody(); 31 | } 32 | 33 | /** 34 | * Get the Array decoded body of the response. 35 | * @return array|mixed 36 | */ 37 | public function array() 38 | { 39 | if (!$this->decoded) { 40 | $this->decoded = json_decode( (string)$this->response->getBody(),true ); 41 | } 42 | 43 | return $this->decoded; 44 | } 45 | 46 | /** 47 | * Get the JSON decoded body of the response. 48 | * @return object|mixed 49 | */ 50 | public function json() 51 | { 52 | if (!$this->decoded) { 53 | $this->decoded = json_decode( (string)$this->response->getBody() ); 54 | } 55 | 56 | return $this->decoded; 57 | } 58 | 59 | /** 60 | * Get a header from the response. 61 | * @param string $header 62 | * @return mixed 63 | */ 64 | public function header(string $header) 65 | { 66 | return $this->response->getHeaderLine( $header ); 67 | } 68 | 69 | /** 70 | * Get the headers from the response. 71 | * @return mixed 72 | */ 73 | public function headers() 74 | { 75 | return $this->mapWithKeys( $this->response->getHeaders(),function ($v,$k) { 76 | return [$k => $v]; 77 | } )->response; 78 | } 79 | 80 | /** 81 | * Get the status code of the response. 82 | * @return int 83 | */ 84 | public function status() 85 | { 86 | return (int)$this->response->getStatusCode(); 87 | } 88 | 89 | /** 90 | * Determine if the request was successful. 91 | * @return bool 92 | */ 93 | public function successful() 94 | { 95 | return $this->status() >= 200 && $this->status() < 300; 96 | } 97 | 98 | /** 99 | * Determine if the response code was "OK". 100 | * @return bool 101 | */ 102 | public function ok() 103 | { 104 | return $this->status() === 200; 105 | } 106 | 107 | /** 108 | * Determine if the response was a redirect. 109 | * @return bool 110 | */ 111 | public function redirect() 112 | { 113 | return $this->status() >= 300 && $this->status() < 400; 114 | } 115 | 116 | /** 117 | * Determine if the response indicates a client error occurred. 118 | * @return bool 119 | */ 120 | public function clientError() 121 | { 122 | return $this->status() >= 400 && $this->status() < 500; 123 | } 124 | 125 | /** 126 | * Determine if the response indicates a server error occurred. 127 | * @return bool 128 | */ 129 | public function serverError() 130 | { 131 | return $this->status() >= 500; 132 | } 133 | 134 | /** 135 | * Determine if the given offset exists. 136 | * 137 | * @param string $offset 138 | * @return mixed 139 | */ 140 | #[\ReturnTypeWillChange] 141 | public function offsetExists($offset) 142 | { 143 | return array_key_exists( $offset,$this->json() ); 144 | } 145 | 146 | /** 147 | * Get the value for a given offset. 148 | * 149 | * @param string $offset 150 | * @return mixed 151 | */ 152 | #[\ReturnTypeWillChange] 153 | public function offsetGet($offset) 154 | { 155 | return $this->json()[$offset]; 156 | } 157 | 158 | /** 159 | * Set the value at the given offset. 160 | * 161 | * @param string $offset 162 | * @param mixed $value 163 | * @return void 164 | * 165 | * @throws \LogicException 166 | */ 167 | #[\ReturnTypeWillChange] 168 | public function offsetSet($offset,$value) 169 | { 170 | throw new LogicException( 'Response data may not be mutated using array access.' ); 171 | } 172 | 173 | /** 174 | * Unset the value at the given offset. 175 | * 176 | * @param string $offset 177 | * @return void 178 | * 179 | * @throws \LogicException 180 | */ 181 | #[\ReturnTypeWillChange] 182 | public function offsetUnset($offset) 183 | { 184 | throw new LogicException( 'Response data may not be mutated using array access.' ); 185 | } 186 | 187 | /** 188 | * Get the body of the response. 189 | * 190 | * @return string 191 | */ 192 | public function __toString() 193 | { 194 | return $this->body(); 195 | } 196 | 197 | protected function mapWithKeys($items,callable $callback) 198 | { 199 | $result = []; 200 | 201 | foreach ( $items as $key => $value ) { 202 | $assoc = $callback( $value,$key ); 203 | 204 | foreach ( $assoc as $mapKey => $mapValue ) { 205 | $result[$mapKey] = $mapValue; 206 | } 207 | } 208 | 209 | return new static( $result ); 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | setLogger($logger); 50 | $this->setFormatter($formatter ?: $this->getDefaultFormatter()); 51 | } 52 | 53 | /** 54 | * Returns the default formatter; 55 | * 56 | * @return MessageFormatter 57 | */ 58 | protected function getDefaultFormatter() 59 | { 60 | return new MessageFormatter(); 61 | } 62 | 63 | /** 64 | * Sets whether requests should be logged before the response is received. 65 | * 66 | * @param boolean $logRequests 67 | */ 68 | public function setRequestLoggingEnabled($logRequests = true) 69 | { 70 | $this->logRequests = (bool) $logRequests; 71 | } 72 | 73 | /** 74 | * Sets the logger, which can be a PSR-3 logger or a callable that accepts 75 | * a log level, message, and array context. 76 | * 77 | * @param LoggerInterface|callable $logger 78 | * 79 | * @throws InvalidArgumentException 80 | */ 81 | public function setLogger($logger) 82 | { 83 | if ($logger instanceof LoggerInterface || is_callable($logger)) { 84 | $this->logger = $logger; 85 | } else { 86 | throw new InvalidArgumentException( 87 | "Logger has to be a Psr\Log\LoggerInterface or callable" 88 | ); 89 | } 90 | } 91 | 92 | /** 93 | * Sets the formatter, which can be a MessageFormatter or callable that 94 | * accepts a request, response, and a reason if an error has occurred. 95 | * 96 | * @param MessageFormatter|callable $formatter 97 | * 98 | * @throws InvalidArgumentException 99 | */ 100 | public function setFormatter($formatter) 101 | { 102 | if ($formatter instanceof MessageFormatter || is_callable($formatter)) { 103 | $this->formatter = $formatter; 104 | } else { 105 | throw new InvalidArgumentException( 106 | "Formatter has to be a \GuzzleHttp\MessageFormatter or callable" 107 | ); 108 | } 109 | } 110 | 111 | /** 112 | * Sets the log level to use, which can be either a string or a callable 113 | * that accepts a response (which could be null). A log level could also 114 | * be null, which indicates that the default log level should be used. 115 | * 116 | * @param string|callable|null 117 | */ 118 | public function setLogLevel($logLevel) 119 | { 120 | $this->logLevel = $logLevel; 121 | } 122 | 123 | /** 124 | * Logs a request and/or a response. 125 | * 126 | * @param RequestInterface $request 127 | * @param ResponseInterface|null $response 128 | * @param $reason 129 | * @return mixed 130 | */ 131 | protected function log( 132 | RequestInterface $request, 133 | ResponseInterface $response = null, 134 | $reason = null 135 | ) { 136 | if ($reason instanceof RequestException) { 137 | $response = $reason->getResponse(); 138 | } 139 | 140 | $level = $this->getLogLevel($response); 141 | $message = $this->getLogMessage($request, $response, $reason); 142 | $context = compact('request', 'response', 'reason'); 143 | 144 | // Make sure that the content of the body is available again. 145 | if ($response) { 146 | $response->getBody()->seek(0);; 147 | } 148 | 149 | if (is_callable($this->logger)) { 150 | return call_user_func($this->logger, $level, $message, $context); 151 | } 152 | 153 | $this->logger->log($level, $message, $context); 154 | } 155 | 156 | /** 157 | * Formats a request and response as a log message. 158 | * 159 | * @param RequestInterface $request 160 | * @param ResponseInterface|null $response 161 | * @param mixed $reason 162 | * 163 | * @return string The formatted message. 164 | */ 165 | protected function getLogMessage( 166 | RequestInterface $request, 167 | ResponseInterface $response = null, 168 | $reason = null 169 | ) { 170 | if ($this->formatter instanceof MessageFormatter) { 171 | return $this->formatter->format( 172 | $request, 173 | $response, 174 | $reason 175 | ); 176 | } 177 | 178 | return call_user_func($this->formatter, $request, $response, $reason); 179 | } 180 | 181 | /** 182 | * Returns a log level for a given response. 183 | * 184 | * @param ResponseInterface $response The response being logged. 185 | * 186 | * @return string LogLevel 187 | */ 188 | protected function getLogLevel(ResponseInterface $response = null) 189 | { 190 | if ( ! $this->logLevel) { 191 | return $this->getDefaultLogLevel($response); 192 | } 193 | 194 | if (is_callable($this->logLevel)) { 195 | return call_user_func($this->logLevel, $response); 196 | } 197 | 198 | return (string) $this->logLevel; 199 | } 200 | 201 | /** 202 | * Returns the default log level for a response. 203 | * 204 | * @param ResponseInterface $response 205 | * 206 | * @return string LogLevel 207 | */ 208 | protected function getDefaultLogLevel(ResponseInterface $response = null) { 209 | if ($response && $response->getStatusCode() >= 300) { 210 | return LogLevel::NOTICE; 211 | } 212 | 213 | return LogLevel::INFO; 214 | } 215 | 216 | /** 217 | * Returns a function which is handled when a request was successful. 218 | * 219 | * @param RequestInterface $request 220 | * 221 | * @return \Closure 222 | */ 223 | protected function onSuccess(RequestInterface $request) 224 | { 225 | return function ($response) use ($request) { 226 | $this->log($request, $response); 227 | return $response; 228 | }; 229 | } 230 | 231 | /** 232 | * Returns a function which is handled when a request was rejected. 233 | * 234 | * @param RequestInterface $request 235 | * 236 | * @return \Closure 237 | */ 238 | protected function onFailure(RequestInterface $request) 239 | { 240 | return function ($reason) use ($request) { 241 | 242 | // Only log a rejected request if it hasn't already been logged. 243 | if ( ! $this->logRequests) { 244 | $this->log($request, null, $reason); 245 | } 246 | 247 | return Promise\rejection_for($reason); 248 | }; 249 | } 250 | 251 | /** 252 | * Called when the middleware is handled by the client. 253 | * 254 | * @param callable $handler 255 | * 256 | * @return \Closure 257 | */ 258 | public function __invoke(callable $handler) 259 | { 260 | return function ($request, array $options) use ($handler) { 261 | 262 | // Only log requests if explicitly set to do so 263 | if ($this->logRequests) { 264 | $this->log($request); 265 | } 266 | 267 | return $handler($request, $options)->then( 268 | $this->onSuccess($request), 269 | $this->onFailure($request) 270 | ); 271 | }; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | EasyHttp 是一个轻量级、语义化、对IDE友好的HTTP客户端,支持常见的HTTP请求、异步请求和并发请求,让你可以快速地使用 HTTP 请求与其他 Web 应用进行通信。 2 | 3 | > EasyHttp并不强制依赖于cURL,如果没有安装cURL,EasyHttp会自动选择使用PHP流处理,或者你也可以提供自己的发送HTTP请求的处理方式。 4 | 5 | 如果您觉得EasyHttp对您有用的话,别忘了给点个赞哦^_^ ! 6 | 7 | github:[github.com/yzh52521/easyhttp](https://github.com/yzh52521/easyhttp "github.com/yzh52521/easyhttp") 8 | 9 | gitee:[gitee.com/yzh52521/easyhttp](https://gitee.com/yzh52521/easyhttp "gitee.com/yzh52521/easyhttp") 10 | 11 | 本包是基于 [ gouguoyin/easyhttp ](https://gitee.com/gouguoyin/easyhttp "gitee.com/gouguoyin/easyhttp") 进行扩展开发,主要实现了以下扩展: 12 | 13 | 1. 增加 retry() 重试机制。 14 | 2. 增加 debug 日志调试功能。 15 | 3. 增加 withHost 指定服务端base_url 16 | 4. 增加 withBody 发送原始数据(Raw)请求 17 | 5. 增加 withMiddleware/withRequestMiddleware/withResponseMiddleware Guzzle 中间件 18 | 6. 增加 connectTimeout 设置等待服务器响应超时 19 | 7. 增加 sink 响应的主体部分将要保存的位置 20 | 8. 增加 maxRedirects 请求的重定向行为最大次数 21 | 22 | 23 | # 安装说明 24 | 25 | #### 环境依赖 26 | 27 | - PHP >= 7.2.5 28 | - 如果使用PHP流处理,allow_url_fopen 必须在php.ini中启用。 29 | - 如果使用cURL处理,cURL >= 7.19.4,并且编译了OpenSSL 与 zlib。 30 | 31 | #### 一键安装 32 | 33 | composer require yzh52521/easyhttp 34 | 35 | ## 发起请求 36 | 37 | #### 同步请求 38 | 39 | ###### 常规请求 40 | 41 | ```php 42 | $response = Http::get('http://httpbin.org/get'); 43 | 44 | $response = Http::get('http://httpbin.org/get?name=yzh52521'); 45 | 46 | $response = Http::get('http://httpbin.org/get?name=yzh52521', ['age' => 18]); 47 | 48 | $response = Http::post('http://httpbin.org/post'); 49 | 50 | $response = Http::post('http://httpbin.org/post', ['name' => 'yzh52521']); 51 | 52 | $response = Http::patch(...); 53 | 54 | $response = Http::put(...); 55 | 56 | $response = Http::delete(...); 57 | 58 | $response = Http::head(...); 59 | 60 | $response = Http::options(...); 61 | 62 | ``` 63 | 64 | ###### 指定服务端base_url的请求 65 | 66 | ```php 67 | // 指定服务端base_url地址,最终请求地址为 https://serv.yzh52521.com/login 68 | $response = Http::withHost('https://serv.yzh52521.com')->post('/login'); 69 | 70 | ``` 71 | ##### 发送原始数据(Raw)请求 72 | ```php 73 | $response = Http::withBody( 74 | base64_encode($photo), 'image/jpeg' 75 | )->post(...); 76 | ``` 77 | ###### 发送 Content-Type 编码请求 78 | 79 | ```php 80 | // application/x-www-form-urlencoded(默认) 81 | $response = Http::asForm()->post(...); 82 | 83 | // application/json 84 | $response = Http::asJson()->post(...); 85 | ``` 86 | 87 | ###### 发送 Multipart 表单请求 88 | 89 | ```php 90 | $response = Http::asMultipart( 91 | 'file_input_name', file_get_contents('photo1.jpg'), 'photo2.jpg' 92 | )->post('http://test.com/attachments'); 93 | 94 | $response = Http::asMultipart( 95 | 'file_input_name', fopen('photo1.jpg', 'r'), 'photo2.jpg' 96 | )->post(...); 97 | 98 | $response = Http::attach( 99 | 'file_input_name', file_get_contents('photo1.jpg'), 'photo2.jpg' 100 | )->post(...); 101 | 102 | $response = Http::attach( 103 | 'file_input_name', fopen('photo1.jpg', 'r'), 'photo2.jpg' 104 | )->post(...); 105 | ``` 106 | > 表单enctype属性需要设置成 multipart/form-data 107 | 108 | ###### 携带请求头的请求 109 | 110 | ```php 111 | $response = Http::withHeaders([ 112 | 'x-powered-by' => 'yzh52521' 113 | ])->post(...); 114 | ``` 115 | 116 | ###### 携带重定向的请求 117 | 118 | ```php 119 | // 默认 120 | $response = Http::withRedirect(false)->post(...); 121 | 122 | $response = Http::withRedirect([ 123 | 'max' => 5, 124 | 'strict' => false, 125 | 'referer' => true, 126 | 'protocols' => ['http', 'https'], 127 | 'track_redirects' => false 128 | ])->post(...); 129 | 130 | $response = Http::maxRedirects(5)->post(...); 131 | ``` 132 | 133 | 134 | 135 | ###### 携带认证的请求 136 | 137 | ```php 138 | // Basic认证 139 | $response = Http::withBasicAuth('username', 'password')->post(...); 140 | 141 | // Digest认证(需要被HTTP服务器支持) 142 | $response = Http::withDigestAuth('username', 'password')->post(...); 143 | ``` 144 | 145 | ###### 携带 User-Agent 的请求 146 | ```php 147 | $response = Http::withUA('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36')->post(...); 148 | ``` 149 | 150 | ###### 携带Token令牌的请求 151 | 152 | ```php 153 | $response = Http::withToken('token')->post(...); 154 | ``` 155 | 156 | ###### 携带认证文件的请求 157 | 158 | ```php 159 | $response = Http::withCert('/path/server.pem', 'password')->post(...); 160 | ``` 161 | 162 | ###### 携带SSL证书的请求 163 | 164 | ```php 165 | // 默认 166 | $response = Http::withVerify(false)->post(...); 167 | 168 | $response = Http::withVerify('/path/to/cert.pem')->post(...); 169 | ``` 170 | 171 | ###### 携带COOKIE的请求 172 | 173 | ```php 174 | $response = Http::withCookies(array $cookies, string $domain)->post(...); 175 | ``` 176 | 177 | ###### 携带协议版本的请求 178 | 179 | ```php 180 | $response = Http::withVersion(1.1)->post(...); 181 | ``` 182 | 183 | ###### 携带代理的请求 184 | 185 | ```php 186 | $response = Http::withProxy('tcp://localhost:8125')->post(...); 187 | 188 | $response = Http::withProxy([ 189 | 'http' => 'tcp://localhost:8125', // Use this proxy with "http" 190 | 'https' => 'tcp://localhost:9124', // Use this proxy with "https", 191 | 'no' => ['.com.cn', 'yzh52521.cn'] // Don't use a proxy with these 192 | ])->post(...); 193 | ``` 194 | 195 | ###### 设置超时时间(单位秒) 196 | 197 | ```php 198 | $response = Http::timeout(60)->post(...); 199 | ``` 200 | 201 | ###### 设置等待服务器响应超时的最大值(单位秒) 202 | 203 | ```php 204 | $response = Http::connectTimeout(60)->post(...); 205 | ``` 206 | 207 | ###### 设置延迟时间(单位秒) 208 | 209 | ```php 210 | $response = Http::delay(60)->post(...); 211 | ``` 212 | 213 | ###### 设置并发次数 214 | 215 | ```php 216 | $response = Http::concurrency(10)->promise(...); 217 | ``` 218 | 219 | ###### 重发请求,设置retry方法。重试次数/两次重试之间的时间间隔(毫秒): 220 | 221 | ```php 222 | $response = Http::retry(3, 100)->post(...); 223 | ``` 224 | ##### 响应的主体部分将要保存的位置 225 | ```php 226 | $response = Http::sink('/path/to/file')->post(...); 227 | ``` 228 | 229 | #### Guzzle 中间件 230 | 231 | ```php 232 | use GuzzleHttp\Middleware; 233 | use Psr\Http\Message\RequestInterface; 234 | use Psr\Http\Message\ResponseInterface; 235 | 236 | $response = Http::withMiddleware( 237 | Middleware::mapRequest(function (RequestInterface $request) { 238 | $request = $request->withHeader('X-Example', 'Value'); 239 | return $request; 240 | }) 241 | )->get('http://example.com'); 242 | 243 | ……………… 244 | $response = Http::withRequestMiddleware( 245 | function (RequestInterface $request) { 246 | $request = $request->withHeader('X-Example', 'Value'); 247 | return $request; 248 | } 249 | )->get('http://example.com'); 250 | 251 | ……………… 252 | 253 | $response = Http::withResponseMiddleware( 254 | function (RequestInterface $response) { 255 | $response = $response->getHeader('X-Example'); 256 | return $response; 257 | } 258 | )->get('http://example.com'); 259 | ``` 260 | 261 | #### 异步请求 262 | 263 | ```php 264 | use yzh52521\EasyHttp\Response; 265 | use yzh52521\EasyHttp\RequestException; 266 | 267 | $promise = Http::getAsync('http://easyhttp.yzh52521.cn/api/sleep3.json', ['token' => TOKEN], function (Response $response) { 268 | echo '异步请求成功,响应内容:' . $response->body() . PHP_EOL; 269 | }, function (RequestException $e) { 270 | echo '异步请求异常,错误码:' . $e->getCode() . ',错误信息:' . $e->getMessage() . PHP_EOL; 271 | }); 272 | 273 | $promise->wait(); 274 | echo json_encode(['code' => 200, 'msg' => '请求成功'], JSON_UNESCAPED_UNICODE) . PHP_EOL; 275 | 276 | //输出 277 | {"code":200,"msg":"请求成功"} 278 | 异步请求成功,响应内容:{"code":200,"msg":"success","second":3} 279 | 280 | $promise = Http::getAsync('http1://easyhttp.yzh52521.cn/api/sleep3.json', function (Response $response) { 281 | echo '异步请求成功,响应内容:' . $response->body() . PHP_EOL; 282 | }, function (RequestException $e) { 283 | echo '异步请求异常,错误信息:' . $e->getMessage() . PHP_EOL; 284 | }); 285 | 286 | $promise->wait(); 287 | echo json_encode(['code' => 200, 'msg' => '请求成功'], JSON_UNESCAPED_UNICODE) . PHP_EOL; 288 | 289 | //输出 290 | {"code":200,"msg":"请求成功"} 291 | 异步请求异常,错误信息:cURL error 1: Protocol "http1" not supported or disabled in libcurl (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) 292 | 293 | Http::postAsync(...); 294 | 295 | Http::patchAsync(...); 296 | 297 | Http::putAsync(...); 298 | 299 | Http::deleteAsync(...); 300 | 301 | Http::headAsync(...); 302 | 303 | Http::optionsAsync(...); 304 | 305 | 使用 等待异步回调处理完成 306 | Http::wait(); 307 | ``` 308 | 309 | #### 异步并发请求 310 | 311 | ```php 312 | use yzh52521\EasyHttp\Response; 313 | use yzh52521\EasyHttp\RequestException; 314 | 315 | $promises = [ 316 | Http::getAsync('http://easyhttp.yzh52521.cn/api/sleep3.json'), 317 | Http::getAsync('http1://easyhttp.yzh52521.cn/api/sleep1.json', ['name' => 'yzh52521']), 318 | Http::postAsync('http://easyhttp.yzh52521.cn/api/sleep2.json', ['name' => 'yzh52521']), 319 | ]; 320 | 321 | $pool=Http::concurrency(10)->multiAsync($promises, function (Response $response, $index) { 322 | echo "发起第 $index 个异步请求,请求时长:" . $response->json()->second . '秒' . PHP_EOL; 323 | }, function (RequestException $e, $index) { 324 | echo "发起第 $index 个请求失败,失败原因:" . $e->getMessage() . PHP_EOL; 325 | }); 326 | 327 | $promise = $pool->promise(); 328 | $promise->wait(); 329 | 330 | //输出 331 | 发起第 1 个请求失败,失败原因:cURL error 1: Protocol "http1" not supported or disabled in libcurl (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) 332 | 发起第 2 个异步请求,请求时长:2 秒 333 | 发起第 0 个异步请求,请求时长:3 秒 334 | ``` 335 | > 如果未调用concurrency()方法,并发次数默认为$promises的元素个数,$promises数组里必须是异步请求 336 | 337 | ## 使用响应 338 | 339 | 发起请求后会返回一个 yzh52521\EasyHttp\Response $response的实例,该实例提供了以下方法来检查请求的响应: 340 | 341 | ```php 342 | $response->body() : string; 343 | $response->json() : object; 344 | $response->array() : array; 345 | $response->status() : int; 346 | $response->ok() : bool; 347 | $response->successful() : bool; 348 | $response->serverError() : bool; 349 | $response->clientError() : bool; 350 | $response->headers() : array; 351 | $response->header($header) : string; 352 | ``` 353 | 354 | ## 异常处理 355 | 356 | 请求在发生客户端或服务端错误时会抛出 yzh52521\EasyHttp\RequestException $e异常,该实例提供了以下方法来返回异常信息: 357 | 358 | ```php 359 | $e->getCode() : int; 360 | $e->getMessage() : string; 361 | $e->getFile() : string; 362 | $e->getLine() : int; 363 | $e->getTrace() : array; 364 | $e->getTraceAsString() : string; 365 | ``` 366 | ## 调试日志 367 | 368 | 有时候难免要对 Http 的请求和响应包体进行记录以方便查找问题或做什么 369 | 370 | ```php 371 | //传递一个日志类 thinkphp \think\facade\Log laravel Illuminate\Support\Facades\Log 372 | Http::debug(Log::class)->post(...); 373 | ``` 374 | 375 | 376 | ## 更新日志 377 | ### 2023-08-31 378 | * 新增 withBody 可以发送原始数据(Raw)请求 379 | * 新增 withMiddleware/withRequestMiddleware/withResponseMiddleware 支持Guzzle中间件 380 | * 新增 connectTimeout 设置等待服务器响应超时 381 | * 新增 sink 响应的主体部分将要保存的位置 382 | * 新增 maxRedirects 请求的重定向行为最大次数 383 | ### 2022-05-11 384 | * 新增removeBodyFormat() 用于withOptions 指定body时,清除原由的bodyFromat 385 | ### 2022-05-10 386 | * 新增发送原生请求的方法client() 387 | * 新增发送原生异步请求的方法clientASync() 388 | ### 2021-09-03 389 | * 新增 debug() 调试日志 390 | * 新增 retry() 重试机制 391 | * 修复header重叠的bug 392 | ### 2020-03-30 393 | * 修复部分情况下IDE不能智能提示的BUG 394 | * get()、getAsync()方法支持带参数的url 395 | * 新增withUA()方法 396 | * 新增withStream()方法 397 | * 新增asMultipart()方法,attach()的别名 398 | * 新增multiAsync()异步并发请求方法 399 | 400 | ### 2020-03-20 401 | * 新增异步请求getAsync()方法 402 | * 新增异步请求postAsync()方法 403 | * 新增异步请求patchAsync()方法 404 | * 新增异步请求putAsync()方法 405 | * 新增异步请求deleteAsync()方法 406 | * 新增异步请求headAsync()方法 407 | * 新增异步请求optionsAsync()方法 408 | 409 | ## Todo List 410 | - [x] 异步请求 411 | - [x] 并发请求 412 | - [x] 重试机制 413 | - [ ] 支持http2 414 | - [ ] 支持swoole 415 | # easyhttp 416 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | client = $this->getInstance(); 86 | 87 | $this->bodyFormat = 'form_params'; 88 | $this->options = [ 89 | 'http_errors' => false, 90 | ]; 91 | $this->handlerStack = HandlerStack::create(new CurlHandler()); 92 | } 93 | 94 | /** 95 | * Request destructor. 96 | */ 97 | public function __destruct() 98 | { 99 | 100 | } 101 | 102 | /** 103 | * 获取单例 104 | * @return mixed 105 | */ 106 | public function getInstance() 107 | { 108 | $name = get_called_class(); 109 | 110 | if (!isset(self::$instances[$name])) { 111 | self::$instances[$name] = new Client(); 112 | } 113 | 114 | return self::$instances[$name]; 115 | } 116 | 117 | public function removeOptions() 118 | { 119 | $this->bodyFormat = 'form_params'; 120 | $this->isRemoveBodyFormat = false; 121 | $this->options = [ 122 | 'http_errors' => false, 123 | 'verify' => false 124 | ]; 125 | return $this; 126 | } 127 | 128 | public function asForm() 129 | { 130 | $this->bodyFormat = 'form_params'; 131 | $this->withHeaders(['Content-Type' => 'application/x-www-form-urlencoded']); 132 | 133 | return $this; 134 | } 135 | 136 | public function asJson() 137 | { 138 | $this->bodyFormat = 'json'; 139 | $this->withHeaders(['Content-Type' => 'application/json']); 140 | 141 | return $this; 142 | } 143 | 144 | public function asMultipart(string $name, string $contents, string $filename = null, array $headers = []) 145 | { 146 | $this->bodyFormat = 'multipart'; 147 | 148 | $this->options = array_filter([ 149 | 'name' => $name, 150 | 'contents' => $contents, 151 | 'headers' => $headers, 152 | 'filename' => $filename, 153 | ]); 154 | 155 | return $this; 156 | } 157 | 158 | public function withMiddleware(callable $middleware) 159 | { 160 | $this->handlerStack->push($middleware); 161 | 162 | $this->options['handler'] = $this->handlerStack; 163 | 164 | return $this; 165 | } 166 | 167 | public function withRequestMiddleware(callable $middleware) 168 | { 169 | $this->handlerStack->push(Middleware::mapRequest($middleware)); 170 | 171 | $this->options['handler'] = $this->handlerStack; 172 | 173 | return $this; 174 | } 175 | 176 | public function withResponseMiddleware(callable $middleware) 177 | { 178 | $this->handlerStack->push(Middleware::mapResponse($middleware)); 179 | 180 | $this->options['handler'] = $this->handlerStack; 181 | 182 | return $this; 183 | } 184 | 185 | public function withHost(string $host) 186 | { 187 | $this->options['base_uri'] = $host; 188 | 189 | return $this; 190 | } 191 | 192 | public function withOptions(array $options) 193 | { 194 | unset($this->options[$this->bodyFormat], $this->options['body']); 195 | 196 | $this->options = array_merge_recursive($this->options, $options); 197 | 198 | return $this; 199 | } 200 | 201 | public function withCert(string $path, string $password) 202 | { 203 | $this->options['cert'] = [$path, $password]; 204 | 205 | return $this; 206 | } 207 | 208 | public function withHeaders(array $headers) 209 | { 210 | $this->options = array_merge_recursive($this->options, [ 211 | 'headers' => $headers, 212 | ]); 213 | 214 | return $this; 215 | } 216 | 217 | public function withBody($content, $contentType = 'application/json') 218 | { 219 | $this->bodyFormat = 'body'; 220 | 221 | $this->options['headers']['Content-Type'] = $contentType; 222 | 223 | $this->pendingBody = $content; 224 | 225 | return $this; 226 | } 227 | 228 | public function withBasicAuth(string $username, string $password) 229 | { 230 | $this->options['auth'] = [$username, $password]; 231 | 232 | return $this; 233 | } 234 | 235 | public function withDigestAuth(string $username, string $password) 236 | { 237 | $this->options['auth'] = [$username, $password, 'digest']; 238 | 239 | return $this; 240 | } 241 | 242 | public function withUA(string $ua) 243 | { 244 | $this->options['headers']['User-Agent'] = trim($ua); 245 | 246 | return $this; 247 | } 248 | 249 | public function withToken(string $token, string $type = 'Bearer') 250 | { 251 | $this->options['headers']['Authorization'] = trim($type . ' ' . $token); 252 | 253 | return $this; 254 | } 255 | 256 | public function withCookies(array $cookies, string $domain) 257 | { 258 | $this->options = array_merge_recursive($this->options, [ 259 | 'cookies' => CookieJar::fromArray($cookies, $domain), 260 | ]); 261 | 262 | return $this; 263 | } 264 | 265 | public function withProxy($proxy) 266 | { 267 | $this->options['proxy'] = $proxy; 268 | 269 | return $this; 270 | } 271 | 272 | public function withVersion($version) 273 | { 274 | $this->options['version'] = $version; 275 | 276 | return $this; 277 | } 278 | 279 | public function maxRedirects(int $max) 280 | { 281 | $this->options['allow_redirects']['max'] = $max; 282 | 283 | return $this; 284 | } 285 | 286 | public function withRedirect($redirect = false) 287 | { 288 | $this->options['allow_redirects'] = $redirect; 289 | 290 | return $this; 291 | } 292 | 293 | public function withVerify($verify = false) 294 | { 295 | $this->options['verify'] = $verify; 296 | 297 | return $this; 298 | } 299 | 300 | public function withStream($boolean = false) 301 | { 302 | $this->options['stream'] = $boolean; 303 | 304 | return $this; 305 | } 306 | 307 | public function concurrency(int $times) 308 | { 309 | $this->concurrency = $times; 310 | 311 | return $this; 312 | } 313 | 314 | public function retry(int $retries = 1, int $sleep = 0) 315 | { 316 | $this->handlerStack->push((new Retry())->handle($retries, $sleep)); 317 | 318 | $this->options['handler'] = $this->handlerStack; 319 | 320 | return $this; 321 | } 322 | 323 | public function delay(int $seconds) 324 | { 325 | $this->options['delay'] = $seconds * 1000; 326 | 327 | return $this; 328 | } 329 | 330 | public function timeout(float $seconds) 331 | { 332 | $this->options['timeout'] = $seconds; 333 | 334 | return $this; 335 | } 336 | 337 | public function connectTimeout(float $seconds) 338 | { 339 | $this->options['connect_timeout'] = $seconds; 340 | 341 | return $this; 342 | } 343 | 344 | /** 345 | * @param string|resource $to 346 | * @return $this 347 | */ 348 | public function sink($to) 349 | { 350 | $this->options['sink'] = $to; 351 | 352 | return $this; 353 | } 354 | 355 | public function removeBodyFormat() 356 | { 357 | $this->isRemoveBodyFormat = true; 358 | return $this; 359 | } 360 | 361 | public function debug($class) 362 | { 363 | $logger = new Logger(function ($level, $message, array $context) use ($class) { 364 | $class::log($level, $message); 365 | }, function ($request, $response, $reason) { 366 | $requestBody = $request->getBody(); 367 | $requestBody->rewind(); 368 | 369 | //请求头 370 | $requestHeaders = []; 371 | 372 | foreach ((array)$request->getHeaders() as $k => $vs) { 373 | foreach ($vs as $v) { 374 | $requestHeaders[] = "$k: $v"; 375 | } 376 | } 377 | 378 | //响应头 379 | $responseHeaders = []; 380 | 381 | foreach ((array)$response->getHeaders() as $k => $vs) { 382 | foreach ($vs as $v) { 383 | $responseHeaders[] = "$k: $v"; 384 | } 385 | } 386 | 387 | $uri = $request->getUri(); 388 | $path = $uri->getPath(); 389 | 390 | if ($query = $uri->getQuery()) { 391 | $path .= '?' . $query; 392 | } 393 | 394 | return sprintf( 395 | "Request %s\n%s %s HTTP/%s\r\n%s\r\n\r\n%s\r\n--------------------\r\nHTTP/%s %s %s\r\n%s\r\n\r\n%s", 396 | $uri, 397 | $request->getMethod(), 398 | $path, 399 | $request->getProtocolVersion(), 400 | join("\r\n", $requestHeaders), 401 | $requestBody->getContents(), 402 | $response->getProtocolVersion(), 403 | $response->getStatusCode(), 404 | $response->getReasonPhrase(), 405 | join("\r\n", $responseHeaders), 406 | $response->getBody()->getContents() 407 | ); 408 | }); 409 | $this->handlerStack->push($logger); 410 | $this->options['handler'] = $this->handlerStack; 411 | 412 | return $this; 413 | } 414 | 415 | public function attach(string $name, string $contents, string $filename = null, array $headers = []) 416 | { 417 | $this->options['multipart'] = array_filter([ 418 | 'name' => $name, 419 | 'contents' => $contents, 420 | 'headers' => $headers, 421 | 'filename' => $filename, 422 | ]); 423 | 424 | return $this; 425 | } 426 | 427 | public function get(string $url, array $query = []) 428 | { 429 | $params = parse_url($url, PHP_URL_QUERY); 430 | 431 | parse_str($params ?: '', $result); 432 | 433 | $this->options['query'] = array_merge($result, $query); 434 | 435 | return $this->request('GET', $url, $query); 436 | } 437 | 438 | public function post(string $url, array $data = []) 439 | { 440 | $this->options[$this->bodyFormat] = $data; 441 | 442 | return $this->request('POST', $url, $data); 443 | } 444 | 445 | public function patch(string $url, array $data = []) 446 | { 447 | $this->options[$this->bodyFormat] = $data; 448 | 449 | return $this->request('PATCH', $url, $data); 450 | } 451 | 452 | public function put(string $url, array $data = []) 453 | { 454 | $this->options[$this->bodyFormat] = $data; 455 | 456 | return $this->request('PUT', $url, $data); 457 | } 458 | 459 | public function delete(string $url, array $data = []) 460 | { 461 | $this->options[$this->bodyFormat] = $data; 462 | 463 | return $this->request('DELETE', $url, $data); 464 | } 465 | 466 | public function head(string $url, array $data = []) 467 | { 468 | $this->options[$this->bodyFormat] = $data; 469 | 470 | return $this->request('HEAD', $url, $data); 471 | } 472 | 473 | public function options(string $url, array $data = []) 474 | { 475 | $this->options[$this->bodyFormat] = $data; 476 | 477 | return $this->request('OPTIONS', $url, $data); 478 | } 479 | 480 | public function getAsync(string $url, $query = null, callable $success = null, callable $fail = null) 481 | { 482 | is_callable($query) || $this->options['query'] = $query; 483 | 484 | return $this->requestAsync('GET', $url, $query, $success, $fail); 485 | } 486 | 487 | public function postAsync(string $url, $data = null, callable $success = null, callable $fail = null) 488 | { 489 | is_callable($data) || $this->options[$this->bodyFormat] = $data; 490 | 491 | return $this->requestAsync('POST', $url, $data, $success, $fail); 492 | } 493 | 494 | public function patchAsync(string $url, $data = null, callable $success = null, callable $fail = null) 495 | { 496 | is_callable($data) || $this->options[$this->bodyFormat] = $data; 497 | 498 | return $this->requestAsync('PATCH', $url, $data, $success, $fail); 499 | } 500 | 501 | public function putAsync(string $url, $data = null, callable $success = null, callable $fail = null) 502 | { 503 | is_callable($data) || $this->options[$this->bodyFormat] = $data; 504 | 505 | return $this->requestAsync('PUT', $url, $data, $success, $fail); 506 | } 507 | 508 | public function deleteAsync(string $url, $data = null, callable $success = null, callable $fail = null) 509 | { 510 | is_callable($data) || $this->options[$this->bodyFormat] = $data; 511 | 512 | return $this->requestAsync('DELETE', $url, $data, $success, $fail); 513 | } 514 | 515 | public function headAsync(string $url, $data = null, callable $success = null, callable $fail = null) 516 | { 517 | is_callable($data) || $this->options[$this->bodyFormat] = $data; 518 | 519 | return $this->requestAsync('HEAD', $url, $data, $success, $fail); 520 | } 521 | 522 | public function optionsAsync(string $url, $data = null, callable $success = null, callable $fail = null) 523 | { 524 | is_callable($data) || $this->options[$this->bodyFormat] = $data; 525 | 526 | return $this->requestAsync('OPTIONS', $url, $data, $success, $fail); 527 | } 528 | 529 | public function multiAsync(array $promises, callable $success = null, callable $fail = null) 530 | { 531 | $count = count($promises); 532 | 533 | $this->concurrency = $this->concurrency ?: $count; 534 | 535 | $requests = function () use ($promises) { 536 | foreach ($promises as $promise) { 537 | yield function () use ($promise) { 538 | return $promise; 539 | }; 540 | } 541 | }; 542 | 543 | $fulfilled = function ($response, $index) use ($success) { 544 | if (!is_null($success)) { 545 | $response = $this->response($response); 546 | call_user_func_array($success, [$response, $index]); 547 | } 548 | }; 549 | 550 | $rejected = function ($exception, $index) use ($fail) { 551 | if (!is_null($fail)) { 552 | $exception = $this->exception($exception); 553 | call_user_func_array($fail, [$exception, $index]); 554 | } 555 | }; 556 | 557 | $pool = new Pool($this->client, $requests(), [ 558 | 'concurrency' => $this->concurrency, 559 | 'fulfilled' => $fulfilled, 560 | 'rejected' => $rejected, 561 | ]); 562 | 563 | $pool->promise(); 564 | 565 | return $pool; 566 | } 567 | 568 | protected function request(string $method, string $url, array $options = []) 569 | { 570 | if (isset($this->options[$this->bodyFormat])) { 571 | $this->options[$this->bodyFormat] = $options; 572 | } else { 573 | $this->options[$this->bodyFormat] = $this->pendingBody; 574 | } 575 | if ($this->isRemoveBodyFormat) { 576 | unset($this->options[$this->bodyFormat]); 577 | } 578 | try { 579 | $response = $this->client->request($method, $url, $this->options); 580 | return $this->response($response); 581 | } catch (ConnectException $e) { 582 | throw new ConnectionException($e->getMessage(), 0, $e); 583 | } 584 | } 585 | 586 | /** 587 | * 原生请求 588 | * @param string $method 589 | * @param string $url 590 | * @param array $options 591 | * @return Response 592 | * @throws ConnectionException 593 | * @throws \GuzzleHttp\Exception\GuzzleException 594 | */ 595 | public function client(string $method, string $url, array $options = []) 596 | { 597 | if (isset($this->options[$this->bodyFormat])) { 598 | $this->options[$this->bodyFormat] = $options; 599 | } else { 600 | $this->options[$this->bodyFormat] = $this->pendingBody; 601 | } 602 | if ($this->isRemoveBodyFormat) { 603 | unset($this->options[$this->bodyFormat]); 604 | } 605 | try { 606 | if (empty($options)) { 607 | $options = $this->options; 608 | } 609 | $response = $this->client->request($method, $url, $options); 610 | return $this->response($response); 611 | } catch (ConnectException $e) { 612 | throw new ConnectionException($e->getMessage(), 0, $e); 613 | } 614 | } 615 | 616 | /** 617 | * 原生异步请求 618 | * @param string $method 619 | * @param string $url 620 | * @param array $options 621 | * @return Response 622 | * @throws ConnectionException 623 | */ 624 | public function clientAsync(string $method, string $url, array $options = []) 625 | { 626 | if (isset($this->options[$this->bodyFormat])) { 627 | $this->options[$this->bodyFormat] = $options; 628 | } else { 629 | $this->options[$this->bodyFormat] = $this->pendingBody; 630 | } 631 | if ($this->isRemoveBodyFormat) { 632 | unset($this->options[$this->bodyFormat]); 633 | } 634 | try { 635 | if (empty($options)) { 636 | $options = $this->options; 637 | } 638 | $response = $this->client->requestAsync($method, $url, $options); 639 | return $this->response($response); 640 | } catch (ConnectException $e) { 641 | throw new ConnectionException($e->getMessage(), 0, $e); 642 | } 643 | } 644 | 645 | 646 | protected function requestAsync(string $method, string $url, $options = null, callable $success = null, callable $fail = null) 647 | { 648 | if (is_callable($options)) { 649 | $successCallback = $options; 650 | $failCallback = $success; 651 | } else { 652 | $successCallback = $success; 653 | $failCallback = $fail; 654 | } 655 | 656 | if (isset($this->options[$this->bodyFormat])) { 657 | $this->options[$this->bodyFormat] = $options; 658 | } else { 659 | $this->options[$this->bodyFormat] = $this->pendingBody; 660 | } 661 | 662 | if ($this->isRemoveBodyFormat) { 663 | unset($this->options[$this->bodyFormat]); 664 | } 665 | 666 | try { 667 | $promise = $this->client->requestAsync($method, $url, $this->options); 668 | 669 | $fulfilled = function ($response) use ($successCallback) { 670 | if (!is_null($successCallback)) { 671 | $response = $this->response($response); 672 | call_user_func_array($successCallback, [$response]); 673 | } 674 | }; 675 | 676 | $rejected = function ($exception) use ($failCallback) { 677 | if (!is_null($failCallback)) { 678 | $exception = $this->exception($exception); 679 | call_user_func_array($failCallback, [$exception]); 680 | } 681 | }; 682 | 683 | $promise->then($fulfilled, $rejected); 684 | 685 | $this->promises[] = $promise; 686 | 687 | return $promise; 688 | } catch (ConnectException $e) { 689 | throw new ConnectionException($e->getMessage(), 0, $e); 690 | } 691 | } 692 | 693 | public function wait() 694 | { 695 | if (!empty($this->promises)) { 696 | \GuzzleHttp\Promise\Utils($this->promises)->wait(); 697 | } 698 | $this->promises = []; 699 | } 700 | 701 | protected function response($response) 702 | { 703 | return new Response($response); 704 | } 705 | 706 | protected function exception($exception) 707 | { 708 | return new RequestException($exception); 709 | } 710 | 711 | } 712 | --------------------------------------------------------------------------------