This commit is contained in:
2024-08-05 22:57:28 +08:00
commit 0efda6c02a
1779 changed files with 171774 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014-present rap2hpoutre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+415
View File
@@ -0,0 +1,415 @@
EasyHttp 是一个轻量级、语义化、对IDE友好的HTTP客户端,支持常见的HTTP请求、异步请求和并发请求,让你可以快速地使用 HTTP 请求与其他 Web 应用进行通信。
> EasyHttp并不强制依赖于cURL,如果没有安装cURL,EasyHttp会自动选择使用PHP流处理,或者你也可以提供自己的发送HTTP请求的处理方式。
如果您觉得EasyHttp对您有用的话,别忘了给点个赞哦^_^ !
github:[github.com/yzh52521/easyhttp](https://github.com/yzh52521/easyhttp "github.com/yzh52521/easyhttp")
gitee:[gitee.com/yzh52521/easyhttp](https://gitee.com/yzh52521/easyhttp "gitee.com/yzh52521/easyhttp")
本包是基于 [ gouguoyin/easyhttp ](https://gitee.com/gouguoyin/easyhttp "gitee.com/gouguoyin/easyhttp") 进行扩展开发,主要实现了以下扩展:
1. 增加 retry() 重试机制。
2. 增加 debug 日志调试功能。
3. 增加 withHost 指定服务端base_url
4. 增加 withBody 发送原始数据(Raw)请求
5. 增加 withMiddleware/withRequestMiddleware/withResponseMiddleware Guzzle 中间件
6. 增加 connectTimeout 设置等待服务器响应超时
7. 增加 sink 响应的主体部分将要保存的位置
8. 增加 maxRedirects 请求的重定向行为最大次数
# 安装说明
#### 环境依赖
- PHP >= 7.2.5
- 如果使用PHP流处理,allow_url_fopen 必须在php.ini中启用。
- 如果使用cURL处理,cURL >= 7.19.4,并且编译了OpenSSL 与 zlib。
#### 一键安装
composer require yzh52521/easyhttp
## 发起请求
#### 同步请求
###### 常规请求
```php
$response = Http::get('http://httpbin.org/get');
$response = Http::get('http://httpbin.org/get?name=yzh52521');
$response = Http::get('http://httpbin.org/get?name=yzh52521', ['age' => 18]);
$response = Http::post('http://httpbin.org/post');
$response = Http::post('http://httpbin.org/post', ['name' => 'yzh52521']);
$response = Http::patch(...);
$response = Http::put(...);
$response = Http::delete(...);
$response = Http::head(...);
$response = Http::options(...);
```
###### 指定服务端base_url的请求
```php
// 指定服务端base_url地址,最终请求地址为 https://serv.yzh52521.com/login
$response = Http::withHost('https://serv.yzh52521.com')->post('/login');
```
##### 发送原始数据(Raw)请求
```php
$response = Http::withBody(
base64_encode($photo), 'image/jpeg'
)->post(...);
```
###### 发送 Content-Type 编码请求
```php
// application/x-www-form-urlencoded(默认)
$response = Http::asForm()->post(...);
// application/json
$response = Http::asJson()->post(...);
```
###### 发送 Multipart 表单请求
```php
$response = Http::asMultipart(
'file_input_name', file_get_contents('photo1.jpg'), 'photo2.jpg'
)->post('http://test.com/attachments');
$response = Http::asMultipart(
'file_input_name', fopen('photo1.jpg', 'r'), 'photo2.jpg'
)->post(...);
$response = Http::attach(
'file_input_name', file_get_contents('photo1.jpg'), 'photo2.jpg'
)->post(...);
$response = Http::attach(
'file_input_name', fopen('photo1.jpg', 'r'), 'photo2.jpg'
)->post(...);
```
> 表单enctype属性需要设置成 multipart/form-data
###### 携带请求头的请求
```php
$response = Http::withHeaders([
'x-powered-by' => 'yzh52521'
])->post(...);
```
###### 携带重定向的请求
```php
// 默认
$response = Http::withRedirect(false)->post(...);
$response = Http::withRedirect([
'max' => 5,
'strict' => false,
'referer' => true,
'protocols' => ['http', 'https'],
'track_redirects' => false
])->post(...);
$response = Http::maxRedirects(5)->post(...);
```
###### 携带认证的请求
```php
// Basic认证
$response = Http::withBasicAuth('username', 'password')->post(...);
// Digest认证(需要被HTTP服务器支持)
$response = Http::withDigestAuth('username', 'password')->post(...);
```
###### 携带 User-Agent 的请求
```php
$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(...);
```
###### 携带Token令牌的请求
```php
$response = Http::withToken('token')->post(...);
```
###### 携带认证文件的请求
```php
$response = Http::withCert('/path/server.pem', 'password')->post(...);
```
###### 携带SSL证书的请求
```php
// 默认
$response = Http::withVerify(false)->post(...);
$response = Http::withVerify('/path/to/cert.pem')->post(...);
```
###### 携带COOKIE的请求
```php
$response = Http::withCookies(array $cookies, string $domain)->post(...);
```
###### 携带协议版本的请求
```php
$response = Http::withVersion(1.1)->post(...);
```
###### 携带代理的请求
```php
$response = Http::withProxy('tcp://localhost:8125')->post(...);
$response = Http::withProxy([
'http' => 'tcp://localhost:8125', // Use this proxy with "http"
'https' => 'tcp://localhost:9124', // Use this proxy with "https",
'no' => ['.com.cn', 'yzh52521.cn'] // Don't use a proxy with these
])->post(...);
```
###### 设置超时时间(单位秒)
```php
$response = Http::timeout(60)->post(...);
```
###### 设置等待服务器响应超时的最大值(单位秒)
```php
$response = Http::connectTimeout(60)->post(...);
```
###### 设置延迟时间(单位秒)
```php
$response = Http::delay(60)->post(...);
```
###### 设置并发次数
```php
$response = Http::concurrency(10)->promise(...);
```
###### 重发请求,设置retry方法。重试次数/两次重试之间的时间间隔(毫秒):
```php
$response = Http::retry(3, 100)->post(...);
```
##### 响应的主体部分将要保存的位置
```php
$response = Http::sink('/path/to/file')->post(...);
```
#### Guzzle 中间件
```php
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
$response = Http::withMiddleware(
Middleware::mapRequest(function (RequestInterface $request) {
$request = $request->withHeader('X-Example', 'Value');
return $request;
})
)->get('http://example.com');
………………
$response = Http::withRequestMiddleware(
function (RequestInterface $request) {
$request = $request->withHeader('X-Example', 'Value');
return $request;
}
)->get('http://example.com');
………………
$response = Http::withResponseMiddleware(
function (RequestInterface $response) {
$response = $response->getHeader('X-Example');
return $response;
}
)->get('http://example.com');
```
#### 异步请求
```php
use yzh52521\EasyHttp\Response;
use yzh52521\EasyHttp\RequestException;
$promise = Http::getAsync('http://easyhttp.yzh52521.cn/api/sleep3.json', ['token' => TOKEN], function (Response $response) {
echo '异步请求成功,响应内容:' . $response->body() . PHP_EOL;
}, function (RequestException $e) {
echo '异步请求异常,错误码:' . $e->getCode() . ',错误信息:' . $e->getMessage() . PHP_EOL;
});
$promise->wait();
echo json_encode(['code' => 200, 'msg' => '请求成功'], JSON_UNESCAPED_UNICODE) . PHP_EOL;
//输出
{"code":200,"msg":"请求成功"}
异步请求成功,响应内容:{"code":200,"msg":"success","second":3}
$promise = Http::getAsync('http1://easyhttp.yzh52521.cn/api/sleep3.json', function (Response $response) {
echo '异步请求成功,响应内容:' . $response->body() . PHP_EOL;
}, function (RequestException $e) {
echo '异步请求异常,错误信息:' . $e->getMessage() . PHP_EOL;
});
$promise->wait();
echo json_encode(['code' => 200, 'msg' => '请求成功'], JSON_UNESCAPED_UNICODE) . PHP_EOL;
//输出
{"code":200,"msg":"请求成功"}
异步请求异常,错误信息:cURL error 1: Protocol "http1" not supported or disabled in libcurl (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)
Http::postAsync(...);
Http::patchAsync(...);
Http::putAsync(...);
Http::deleteAsync(...);
Http::headAsync(...);
Http::optionsAsync(...);
使用 等待异步回调处理完成
Http::wait();
```
#### 异步并发请求
```php
use yzh52521\EasyHttp\Response;
use yzh52521\EasyHttp\RequestException;
$promises = [
Http::getAsync('http://easyhttp.yzh52521.cn/api/sleep3.json'),
Http::getAsync('http1://easyhttp.yzh52521.cn/api/sleep1.json', ['name' => 'yzh52521']),
Http::postAsync('http://easyhttp.yzh52521.cn/api/sleep2.json', ['name' => 'yzh52521']),
];
$pool=Http::concurrency(10)->multiAsync($promises, function (Response $response, $index) {
echo "发起第 $index 个异步请求,请求时长:" . $response->json()->second . '秒' . PHP_EOL;
}, function (RequestException $e, $index) {
echo "发起第 $index 个请求失败,失败原因:" . $e->getMessage() . PHP_EOL;
});
$promise = $pool->promise();
$promise->wait();
//输出
发起第 1 个请求失败,失败原因:cURL error 1: Protocol "http1" not supported or disabled in libcurl (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)
发起第 2 个异步请求,请求时长:2
发起第 0 个异步请求,请求时长:3
```
> 如果未调用concurrency()方法,并发次数默认为$promises的元素个数,$promises数组里必须是异步请求
## 使用响应
发起请求后会返回一个 yzh52521\EasyHttp\Response $response的实例,该实例提供了以下方法来检查请求的响应:
```php
$response->body() : string;
$response->json() : object;
$response->array() : array;
$response->status() : int;
$response->ok() : bool;
$response->successful() : bool;
$response->serverError() : bool;
$response->clientError() : bool;
$response->headers() : array;
$response->header($header) : string;
```
## 异常处理
请求在发生客户端或服务端错误时会抛出 yzh52521\EasyHttp\RequestException $e异常,该实例提供了以下方法来返回异常信息:
```php
$e->getCode() : int;
$e->getMessage() : string;
$e->getFile() : string;
$e->getLine() : int;
$e->getTrace() : array;
$e->getTraceAsString() : string;
```
## 调试日志
有时候难免要对 Http 的请求和响应包体进行记录以方便查找问题或做什么
```php
//传递一个日志类 thinkphp \think\facade\Log laravel Illuminate\Support\Facades\Log
Http::debug(Log::class)->post(...);
```
## 更新日志
### 2023-08-31
* 新增 withBody 可以发送原始数据(Raw)请求
* 新增 withMiddleware/withRequestMiddleware/withResponseMiddleware 支持Guzzle中间件
* 新增 connectTimeout 设置等待服务器响应超时
* 新增 sink 响应的主体部分将要保存的位置
* 新增 maxRedirects 请求的重定向行为最大次数
### 2022-05-11
* 新增removeBodyFormat() 用于withOptions 指定body时,清除原由的bodyFromat
### 2022-05-10
* 新增发送原生请求的方法client()
* 新增发送原生异步请求的方法clientASync()
### 2021-09-03
* 新增 debug() 调试日志
* 新增 retry() 重试机制
* 修复header重叠的bug
### 2020-03-30
* 修复部分情况下IDE不能智能提示的BUG
* get()、getAsync()方法支持带参数的url
* 新增withUA()方法
* 新增withStream()方法
* 新增asMultipart()方法,attach()的别名
* 新增multiAsync()异步并发请求方法
### 2020-03-20
* 新增异步请求getAsync()方法
* 新增异步请求postAsync()方法
* 新增异步请求patchAsync()方法
* 新增异步请求putAsync()方法
* 新增异步请求deleteAsync()方法
* 新增异步请求headAsync()方法
* 新增异步请求optionsAsync()方法
## Todo List
- [x] 异步请求
- [x] 并发请求
- [x] 重试机制
- [ ] 支持http2
- [ ] 支持swoole
# easyhttp
+33
View File
@@ -0,0 +1,33 @@
{
"name": "yzh52521/easyhttp",
"description": "EasyHttp 是一个轻量级、语义化、对IDE友好的HTTP客户端,支持常见的HTTP请求、异步请求和并发请求,让你可以快速地使用 HTTP 请求与其他 Web 应用进行通信。",
"license": "MIT",
"keywords": [
"easyhttp",
"EasyHttp",
"php-http",
"phphttp",
"easy-http",
"php",
"http",
"curl"
],
"homepage": "https://github.com/yzh52521/easyhttp",
"authors": [
{
"name": "yzh52521",
"email": "396751927@qq.com"
}
],
"require": {
"php": ">=7.2.5",
"guzzlehttp/guzzle": "^6.0|^7.0",
"psr/log":"^1.0|^2.0|^3.0"
},
"autoload": {
"psr-4": {
"yzh52521\\EasyHttp\\": "src/"
}
},
"minimum-stability": "stable"
}
@@ -0,0 +1,10 @@
<?php
namespace yzh52521\EasyHttp;
use Exception;
class ConnectionException extends Exception
{
//
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace yzh52521\EasyHttp;
class Facade
{
protected $facade;
public function __construct()
{
$this->facade = new $this->facade;
}
public function __call($name, $params)
{
if (method_exists($this->facade, 'removeOptions')) {
call_user_func_array([$this->facade, 'removeOptions'], []);
}
return call_user_func_array([$this->facade, $name], $params);
}
public static function __callStatic($name, $params)
{
return call_user_func_array([new static(), $name], $params);
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace yzh52521\EasyHttp;
/**
* @method static \yzh52521\EasyHttp\Request asJson()
* @method static \yzh52521\EasyHttp\Request asForm()
* @method static \yzh52521\EasyHttp\Request asMultipart(string $name, string $contents, string|null $filename = null, array $headers)
* @method static \yzh52521\EasyHttp\Request attach(string $name, string $contents, string|null $filename = null, array $headers)
*
* @method static \yzh52521\EasyHttp\Request withRedirect(bool|array $redirect)
* @method static \yzh52521\EasyHttp\Request withStream(bool $boolean)
* @method static \yzh52521\EasyHttp\Request withVerify(bool|string $verify)
* @method static \yzh52521\EasyHttp\Request withHost(string $host)
* @method static \yzh52521\EasyHttp\Request withHeaders(array $headers)
* @method static \yzh52521\EasyHttp\Request withBody($content,$contentType='application/json')
* @method static \yzh52521\EasyHttp\Request withBasicAuth(string $username, string $password)
* @method static \yzh52521\EasyHttp\Request withDigestAuth(string $username, string $password)
* @method static \yzh52521\EasyHttp\Request withUA(string $ua)
* @method static \yzh52521\EasyHttp\Request withToken(string $token, string $type = 'Bearer')
* @method static \yzh52521\EasyHttp\Request withCookies(array $cookies, string $domain)
* @method static \yzh52521\EasyHttp\Request withProxy(string|array $proxy)
* @method static \yzh52521\EasyHttp\Request withVersion(string $version)
* @method static \yzh52521\EasyHttp\Request withOptions(array $options)
* @method static \yzh52521\EasyHttp\Request withMiddleware(callable $middleware)
* @method static \yzh52521\EasyHttp\Request withRequestMiddleware(callable $middleware)
* @method static \yzh52521\EasyHttp\Request withResponseMiddleware(callable $middleware)
*
* @method static \yzh52521\EasyHttp\Request debug($class)
* @method static \yzh52521\EasyHttp\Request retry(int $retries=1,int $sleep=0)
* @method static \yzh52521\EasyHttp\Request delay(int $seconds)
* @method static \yzh52521\EasyHttp\Request timeout(float $seconds)
* @method static \yzh52521\EasyHttp\Request connectTimeout(float $seconds)
* @method static \yzh52521\EasyHttp\Request sink(string|resource $to)
* @method static \yzh52521\EasyHttp\Request concurrency(int $times)
* @method static \yzh52521\EasyHttp\Request removeBodyFormat()
* @method static \yzh52521\EasyHttp\Request maxRedirects(int $max)
*
* @method static \yzh52521\EasyHttp\Response get(string $url, array $query = [])
* @method static \yzh52521\EasyHttp\Response post(string $url, array $data = [])
* @method static \yzh52521\EasyHttp\Response patch(string $url, array $data = [])
* @method static \yzh52521\EasyHttp\Response put(string $url, array $data = [])
* @method static \yzh52521\EasyHttp\Response delete(string $url, array $data = [])
* @method static \yzh52521\EasyHttp\Response head(string $url, array $data = [])
* @method static \yzh52521\EasyHttp\Response options(string $url, array $data = [])
* @method static \yzh52521\EasyHttp\Response client(string $method, string $url, array $options = [])
* @method static \yzh52521\EasyHttp\Response clientAsync(string $method, string $url, array $options = [])
*
* @method static \GuzzleHttp\Promise\PromiseInterface getAsync(string $url, array|null $query = null, callable $success = null, callable $fail = null)
* @method static \GuzzleHttp\Promise\PromiseInterface postAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
* @method static \GuzzleHttp\Promise\PromiseInterface patchAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
* @method static \GuzzleHttp\Promise\PromiseInterface putAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
* @method static \GuzzleHttp\Promise\PromiseInterface deleteAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
* @method static \GuzzleHttp\Promise\PromiseInterface headAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
* @method static \GuzzleHttp\Promise\PromiseInterface optionsAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
* @method static \GuzzleHttp\Pool multiAsync(array $promises, callable $success = null, callable $fail = null)
* @method static void wait()
*/
class Http extends Facade
{
protected $facade = Request::class;
}
+273
View File
@@ -0,0 +1,273 @@
<?php
namespace yzh52521\EasyHttp;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LogLevel;
use Psr\Log\LoggerInterface;
use InvalidArgumentException;
/**
* Guzzle middleware which logs a request and its response.
*/
class Logger
{
/**
* @var \Psr\Log\LoggerInterface|callable
*/
protected $logger;
/**
* @var \GuzzleHttp\MessageFormatter|callable
*/
protected $formatter;
/**
* @var string|callable Constant or callable that accepts a Response.
*/
protected $logLevel;
/**
* @var boolean Whether or not to log requests as they are made.
*/
protected $logRequests;
/**
* Creates a callable middleware for logging requests and responses.
*
* @param LoggerInterface|callable $logger
* @param string|callable Constant or callable that accepts a Response.
*/
public function __construct($logger, $formatter = null)
{
// Use the setters to take care of type validation
$this->setLogger($logger);
$this->setFormatter($formatter ?: $this->getDefaultFormatter());
}
/**
* Returns the default formatter;
*
* @return MessageFormatter
*/
protected function getDefaultFormatter()
{
return new MessageFormatter();
}
/**
* Sets whether requests should be logged before the response is received.
*
* @param boolean $logRequests
*/
public function setRequestLoggingEnabled($logRequests = true)
{
$this->logRequests = (bool) $logRequests;
}
/**
* Sets the logger, which can be a PSR-3 logger or a callable that accepts
* a log level, message, and array context.
*
* @param LoggerInterface|callable $logger
*
* @throws InvalidArgumentException
*/
public function setLogger($logger)
{
if ($logger instanceof LoggerInterface || is_callable($logger)) {
$this->logger = $logger;
} else {
throw new InvalidArgumentException(
"Logger has to be a Psr\Log\LoggerInterface or callable"
);
}
}
/**
* Sets the formatter, which can be a MessageFormatter or callable that
* accepts a request, response, and a reason if an error has occurred.
*
* @param MessageFormatter|callable $formatter
*
* @throws InvalidArgumentException
*/
public function setFormatter($formatter)
{
if ($formatter instanceof MessageFormatter || is_callable($formatter)) {
$this->formatter = $formatter;
} else {
throw new InvalidArgumentException(
"Formatter has to be a \GuzzleHttp\MessageFormatter or callable"
);
}
}
/**
* Sets the log level to use, which can be either a string or a callable
* that accepts a response (which could be null). A log level could also
* be null, which indicates that the default log level should be used.
*
* @param string|callable|null
*/
public function setLogLevel($logLevel)
{
$this->logLevel = $logLevel;
}
/**
* Logs a request and/or a response.
*
* @param RequestInterface $request
* @param ResponseInterface|null $response
* @param $reason
* @return mixed
*/
protected function log(
RequestInterface $request,
ResponseInterface $response = null,
$reason = null
) {
if ($reason instanceof RequestException) {
$response = $reason->getResponse();
}
$level = $this->getLogLevel($response);
$message = $this->getLogMessage($request, $response, $reason);
$context = compact('request', 'response', 'reason');
// Make sure that the content of the body is available again.
if ($response) {
$response->getBody()->seek(0);;
}
if (is_callable($this->logger)) {
return call_user_func($this->logger, $level, $message, $context);
}
$this->logger->log($level, $message, $context);
}
/**
* Formats a request and response as a log message.
*
* @param RequestInterface $request
* @param ResponseInterface|null $response
* @param mixed $reason
*
* @return string The formatted message.
*/
protected function getLogMessage(
RequestInterface $request,
ResponseInterface $response = null,
$reason = null
) {
if ($this->formatter instanceof MessageFormatter) {
return $this->formatter->format(
$request,
$response,
$reason
);
}
return call_user_func($this->formatter, $request, $response, $reason);
}
/**
* Returns a log level for a given response.
*
* @param ResponseInterface $response The response being logged.
*
* @return string LogLevel
*/
protected function getLogLevel(ResponseInterface $response = null)
{
if ( ! $this->logLevel) {
return $this->getDefaultLogLevel($response);
}
if (is_callable($this->logLevel)) {
return call_user_func($this->logLevel, $response);
}
return (string) $this->logLevel;
}
/**
* Returns the default log level for a response.
*
* @param ResponseInterface $response
*
* @return string LogLevel
*/
protected function getDefaultLogLevel(ResponseInterface $response = null) {
if ($response && $response->getStatusCode() >= 300) {
return LogLevel::NOTICE;
}
return LogLevel::INFO;
}
/**
* Returns a function which is handled when a request was successful.
*
* @param RequestInterface $request
*
* @return \Closure
*/
protected function onSuccess(RequestInterface $request)
{
return function ($response) use ($request) {
$this->log($request, $response);
return $response;
};
}
/**
* Returns a function which is handled when a request was rejected.
*
* @param RequestInterface $request
*
* @return \Closure
*/
protected function onFailure(RequestInterface $request)
{
return function ($reason) use ($request) {
// Only log a rejected request if it hasn't already been logged.
if ( ! $this->logRequests) {
$this->log($request, null, $reason);
}
return Promise\rejection_for($reason);
};
}
/**
* Called when the middleware is handled by the client.
*
* @param callable $handler
*
* @return \Closure
*/
public function __invoke(callable $handler)
{
return function ($request, array $options) use ($handler) {
// Only log requests if explicitly set to do so
if ($this->logRequests) {
$this->log($request);
}
return $handler($request, $options)->then(
$this->onSuccess($request),
$this->onFailure($request)
);
};
}
}
+711
View File
@@ -0,0 +1,711 @@
<?php
namespace yzh52521\EasyHttp;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\ConnectException;
/**
* @method \yzh52521\EasyHttp\Response body()
* @method \yzh52521\EasyHttp\Response array()
* @method \yzh52521\EasyHttp\Response json()
* @method \yzh52521\EasyHttp\Response headers()
* @method \yzh52521\EasyHttp\Response header(string $header)
* @method \yzh52521\EasyHttp\Response status()
* @method \yzh52521\EasyHttp\Response successful()
* @method \yzh52521\EasyHttp\Response ok()
* @method \yzh52521\EasyHttp\Response redirect()
* @method \yzh52521\EasyHttp\Response clientError()
* @method \yzh52521\EasyHttp\Response serverError()
*/
class Request
{
/**
* \GuzzleHttp\Client单例
* @var array
*/
private static $instances = [];
/**
* \GuzzleHttp\Client;
* @var Client
*/
protected $client;
/**
* Body格式
* @var string
*/
protected $bodyFormat;
/**
* The raw body for the request.
*
* @var string
*/
protected $pendingBody;
protected $isRemoveBodyFormat = false;
/**
* @var array
*/
protected $options = [];
/**
* @var array
*/
protected $promises = [];
/**
* 并发次数
* @var
*/
protected $concurrency;
/**
*
* @var HandlerStack
*/
protected $handlerStack;
/**
* Request constructor.
*/
public function __construct()
{
$this->client = $this->getInstance();
$this->bodyFormat = 'form_params';
$this->options = [
'http_errors' => false,
];
$this->handlerStack = HandlerStack::create(new CurlHandler());
}
/**
* Request destructor.
*/
public function __destruct()
{
}
/**
* 获取单例
* @return mixed
*/
public function getInstance()
{
$name = get_called_class();
if (!isset(self::$instances[$name])) {
self::$instances[$name] = new Client();
}
return self::$instances[$name];
}
public function removeOptions()
{
$this->bodyFormat = 'form_params';
$this->isRemoveBodyFormat = false;
$this->options = [
'http_errors' => false,
'verify' => false
];
return $this;
}
public function asForm()
{
$this->bodyFormat = 'form_params';
$this->withHeaders(['Content-Type' => 'application/x-www-form-urlencoded']);
return $this;
}
public function asJson()
{
$this->bodyFormat = 'json';
$this->withHeaders(['Content-Type' => 'application/json']);
return $this;
}
public function asMultipart(string $name, string $contents, string $filename = null, array $headers = [])
{
$this->bodyFormat = 'multipart';
$this->options = array_filter([
'name' => $name,
'contents' => $contents,
'headers' => $headers,
'filename' => $filename,
]);
return $this;
}
public function withMiddleware(callable $middleware)
{
$this->handlerStack->push($middleware);
$this->options['handler'] = $this->handlerStack;
return $this;
}
public function withRequestMiddleware(callable $middleware)
{
$this->handlerStack->push(Middleware::mapRequest($middleware));
$this->options['handler'] = $this->handlerStack;
return $this;
}
public function withResponseMiddleware(callable $middleware)
{
$this->handlerStack->push(Middleware::mapResponse($middleware));
$this->options['handler'] = $this->handlerStack;
return $this;
}
public function withHost(string $host)
{
$this->options['base_uri'] = $host;
return $this;
}
public function withOptions(array $options)
{
unset($this->options[$this->bodyFormat], $this->options['body']);
$this->options = array_merge_recursive($this->options, $options);
return $this;
}
public function withCert(string $path, string $password)
{
$this->options['cert'] = [$path, $password];
return $this;
}
public function withHeaders(array $headers)
{
$this->options = array_merge_recursive($this->options, [
'headers' => $headers,
]);
return $this;
}
public function withBody($content, $contentType = 'application/json')
{
$this->bodyFormat = 'body';
$this->options['headers']['Content-Type'] = $contentType;
$this->pendingBody = $content;
return $this;
}
public function withBasicAuth(string $username, string $password)
{
$this->options['auth'] = [$username, $password];
return $this;
}
public function withDigestAuth(string $username, string $password)
{
$this->options['auth'] = [$username, $password, 'digest'];
return $this;
}
public function withUA(string $ua)
{
$this->options['headers']['User-Agent'] = trim($ua);
return $this;
}
public function withToken(string $token, string $type = 'Bearer')
{
$this->options['headers']['Authorization'] = trim($type . ' ' . $token);
return $this;
}
public function withCookies(array $cookies, string $domain)
{
$this->options = array_merge_recursive($this->options, [
'cookies' => CookieJar::fromArray($cookies, $domain),
]);
return $this;
}
public function withProxy($proxy)
{
$this->options['proxy'] = $proxy;
return $this;
}
public function withVersion($version)
{
$this->options['version'] = $version;
return $this;
}
public function maxRedirects(int $max)
{
$this->options['allow_redirects']['max'] = $max;
return $this;
}
public function withRedirect($redirect = false)
{
$this->options['allow_redirects'] = $redirect;
return $this;
}
public function withVerify($verify = false)
{
$this->options['verify'] = $verify;
return $this;
}
public function withStream($boolean = false)
{
$this->options['stream'] = $boolean;
return $this;
}
public function concurrency(int $times)
{
$this->concurrency = $times;
return $this;
}
public function retry(int $retries = 1, int $sleep = 0)
{
$this->handlerStack->push((new Retry())->handle($retries, $sleep));
$this->options['handler'] = $this->handlerStack;
return $this;
}
public function delay(int $seconds)
{
$this->options['delay'] = $seconds * 1000;
return $this;
}
public function timeout(float $seconds)
{
$this->options['timeout'] = $seconds;
return $this;
}
public function connectTimeout(float $seconds)
{
$this->options['connect_timeout'] = $seconds;
return $this;
}
/**
* @param string|resource $to
* @return $this
*/
public function sink($to)
{
$this->options['sink'] = $to;
return $this;
}
public function removeBodyFormat()
{
$this->isRemoveBodyFormat = true;
return $this;
}
public function debug($class)
{
$logger = new Logger(function ($level, $message, array $context) use ($class) {
$class::log($level, $message);
}, function ($request, $response, $reason) {
$requestBody = $request->getBody();
$requestBody->rewind();
//请求头
$requestHeaders = [];
foreach ((array)$request->getHeaders() as $k => $vs) {
foreach ($vs as $v) {
$requestHeaders[] = "$k: $v";
}
}
//响应头
$responseHeaders = [];
foreach ((array)$response->getHeaders() as $k => $vs) {
foreach ($vs as $v) {
$responseHeaders[] = "$k: $v";
}
}
$uri = $request->getUri();
$path = $uri->getPath();
if ($query = $uri->getQuery()) {
$path .= '?' . $query;
}
return sprintf(
"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",
$uri,
$request->getMethod(),
$path,
$request->getProtocolVersion(),
join("\r\n", $requestHeaders),
$requestBody->getContents(),
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase(),
join("\r\n", $responseHeaders),
$response->getBody()->getContents()
);
});
$this->handlerStack->push($logger);
$this->options['handler'] = $this->handlerStack;
return $this;
}
public function attach(string $name, string $contents, string $filename = null, array $headers = [])
{
$this->options['multipart'] = array_filter([
'name' => $name,
'contents' => $contents,
'headers' => $headers,
'filename' => $filename,
]);
return $this;
}
public function get(string $url, array $query = [])
{
$params = parse_url($url, PHP_URL_QUERY);
parse_str($params ?: '', $result);
$this->options['query'] = array_merge($result, $query);
return $this->request('GET', $url, $query);
}
public function post(string $url, array $data = [])
{
$this->options[$this->bodyFormat] = $data;
return $this->request('POST', $url, $data);
}
public function patch(string $url, array $data = [])
{
$this->options[$this->bodyFormat] = $data;
return $this->request('PATCH', $url, $data);
}
public function put(string $url, array $data = [])
{
$this->options[$this->bodyFormat] = $data;
return $this->request('PUT', $url, $data);
}
public function delete(string $url, array $data = [])
{
$this->options[$this->bodyFormat] = $data;
return $this->request('DELETE', $url, $data);
}
public function head(string $url, array $data = [])
{
$this->options[$this->bodyFormat] = $data;
return $this->request('HEAD', $url, $data);
}
public function options(string $url, array $data = [])
{
$this->options[$this->bodyFormat] = $data;
return $this->request('OPTIONS', $url, $data);
}
public function getAsync(string $url, $query = null, callable $success = null, callable $fail = null)
{
is_callable($query) || $this->options['query'] = $query;
return $this->requestAsync('GET', $url, $query, $success, $fail);
}
public function postAsync(string $url, $data = null, callable $success = null, callable $fail = null)
{
is_callable($data) || $this->options[$this->bodyFormat] = $data;
return $this->requestAsync('POST', $url, $data, $success, $fail);
}
public function patchAsync(string $url, $data = null, callable $success = null, callable $fail = null)
{
is_callable($data) || $this->options[$this->bodyFormat] = $data;
return $this->requestAsync('PATCH', $url, $data, $success, $fail);
}
public function putAsync(string $url, $data = null, callable $success = null, callable $fail = null)
{
is_callable($data) || $this->options[$this->bodyFormat] = $data;
return $this->requestAsync('PUT', $url, $data, $success, $fail);
}
public function deleteAsync(string $url, $data = null, callable $success = null, callable $fail = null)
{
is_callable($data) || $this->options[$this->bodyFormat] = $data;
return $this->requestAsync('DELETE', $url, $data, $success, $fail);
}
public function headAsync(string $url, $data = null, callable $success = null, callable $fail = null)
{
is_callable($data) || $this->options[$this->bodyFormat] = $data;
return $this->requestAsync('HEAD', $url, $data, $success, $fail);
}
public function optionsAsync(string $url, $data = null, callable $success = null, callable $fail = null)
{
is_callable($data) || $this->options[$this->bodyFormat] = $data;
return $this->requestAsync('OPTIONS', $url, $data, $success, $fail);
}
public function multiAsync(array $promises, callable $success = null, callable $fail = null)
{
$count = count($promises);
$this->concurrency = $this->concurrency ?: $count;
$requests = function () use ($promises) {
foreach ($promises as $promise) {
yield function () use ($promise) {
return $promise;
};
}
};
$fulfilled = function ($response, $index) use ($success) {
if (!is_null($success)) {
$response = $this->response($response);
call_user_func_array($success, [$response, $index]);
}
};
$rejected = function ($exception, $index) use ($fail) {
if (!is_null($fail)) {
$exception = $this->exception($exception);
call_user_func_array($fail, [$exception, $index]);
}
};
$pool = new Pool($this->client, $requests(), [
'concurrency' => $this->concurrency,
'fulfilled' => $fulfilled,
'rejected' => $rejected,
]);
$pool->promise();
return $pool;
}
protected function request(string $method, string $url, array $options = [])
{
if (isset($this->options[$this->bodyFormat])) {
$this->options[$this->bodyFormat] = $options;
} else {
$this->options[$this->bodyFormat] = $this->pendingBody;
}
if ($this->isRemoveBodyFormat) {
unset($this->options[$this->bodyFormat]);
}
try {
$response = $this->client->request($method, $url, $this->options);
return $this->response($response);
} catch (ConnectException $e) {
throw new ConnectionException($e->getMessage(), 0, $e);
}
}
/**
* 原生请求
* @param string $method
* @param string $url
* @param array $options
* @return Response
* @throws ConnectionException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function client(string $method, string $url, array $options = [])
{
if (isset($this->options[$this->bodyFormat])) {
$this->options[$this->bodyFormat] = $options;
} else {
$this->options[$this->bodyFormat] = $this->pendingBody;
}
if ($this->isRemoveBodyFormat) {
unset($this->options[$this->bodyFormat]);
}
try {
if (empty($options)) {
$options = $this->options;
}
$response = $this->client->request($method, $url, $options);
return $this->response($response);
} catch (ConnectException $e) {
throw new ConnectionException($e->getMessage(), 0, $e);
}
}
/**
* 原生异步请求
* @param string $method
* @param string $url
* @param array $options
* @return Response
* @throws ConnectionException
*/
public function clientAsync(string $method, string $url, array $options = [])
{
if (isset($this->options[$this->bodyFormat])) {
$this->options[$this->bodyFormat] = $options;
} else {
$this->options[$this->bodyFormat] = $this->pendingBody;
}
if ($this->isRemoveBodyFormat) {
unset($this->options[$this->bodyFormat]);
}
try {
if (empty($options)) {
$options = $this->options;
}
$response = $this->client->requestAsync($method, $url, $options);
return $this->response($response);
} catch (ConnectException $e) {
throw new ConnectionException($e->getMessage(), 0, $e);
}
}
protected function requestAsync(string $method, string $url, $options = null, callable $success = null, callable $fail = null)
{
if (is_callable($options)) {
$successCallback = $options;
$failCallback = $success;
} else {
$successCallback = $success;
$failCallback = $fail;
}
if (isset($this->options[$this->bodyFormat])) {
$this->options[$this->bodyFormat] = $options;
} else {
$this->options[$this->bodyFormat] = $this->pendingBody;
}
if ($this->isRemoveBodyFormat) {
unset($this->options[$this->bodyFormat]);
}
try {
$promise = $this->client->requestAsync($method, $url, $this->options);
$fulfilled = function ($response) use ($successCallback) {
if (!is_null($successCallback)) {
$response = $this->response($response);
call_user_func_array($successCallback, [$response]);
}
};
$rejected = function ($exception) use ($failCallback) {
if (!is_null($failCallback)) {
$exception = $this->exception($exception);
call_user_func_array($failCallback, [$exception]);
}
};
$promise->then($fulfilled, $rejected);
$this->promises[] = $promise;
return $promise;
} catch (ConnectException $e) {
throw new ConnectionException($e->getMessage(), 0, $e);
}
}
public function wait()
{
if (!empty($this->promises)) {
\GuzzleHttp\Promise\Utils($this->promises)->wait();
}
$this->promises = [];
}
protected function response($response)
{
return new Response($response);
}
protected function exception($exception)
{
return new RequestException($exception);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace yzh52521\EasyHttp;
class RequestException
{
public $exception;
public function __construct($exception)
{
$this->exception = $exception;
}
public function getCode()
{
return $this->exception->getCode();
}
public function getMessage()
{
return $this->exception->getMessage();
}
public function getFile()
{
return $this->exception->getFile();
}
public function getLine()
{
return $this->exception->getLine();
}
public function getTrace()
{
return $this->exception->getTrace();
}
public function getTraceAsString()
{
return $this->exception->getTraceAsString();
}
}
+212
View File
@@ -0,0 +1,212 @@
<?php
namespace yzh52521\EasyHttp;
use ArrayAccess;
use LogicException;
class Response implements ArrayAccess
{
protected $response;
/**
* The decoded JSON response.
*
* @var array
*/
protected $decoded;
public function __construct($response)
{
$this->response = $response;
}
/**
* Get the body of the response.
* @return string
*/
public function body()
{
return (string)$this->response->getBody();
}
/**
* Get the Array decoded body of the response.
* @return array|mixed
*/
public function array()
{
if (!$this->decoded) {
$this->decoded = json_decode( (string)$this->response->getBody(),true );
}
return $this->decoded;
}
/**
* Get the JSON decoded body of the response.
* @return object|mixed
*/
public function json()
{
if (!$this->decoded) {
$this->decoded = json_decode( (string)$this->response->getBody() );
}
return $this->decoded;
}
/**
* Get a header from the response.
* @param string $header
* @return mixed
*/
public function header(string $header)
{
return $this->response->getHeaderLine( $header );
}
/**
* Get the headers from the response.
* @return mixed
*/
public function headers()
{
return $this->mapWithKeys( $this->response->getHeaders(),function ($v,$k) {
return [$k => $v];
} )->response;
}
/**
* Get the status code of the response.
* @return int
*/
public function status()
{
return (int)$this->response->getStatusCode();
}
/**
* Determine if the request was successful.
* @return bool
*/
public function successful()
{
return $this->status() >= 200 && $this->status() < 300;
}
/**
* Determine if the response code was "OK".
* @return bool
*/
public function ok()
{
return $this->status() === 200;
}
/**
* Determine if the response was a redirect.
* @return bool
*/
public function redirect()
{
return $this->status() >= 300 && $this->status() < 400;
}
/**
* Determine if the response indicates a client error occurred.
* @return bool
*/
public function clientError()
{
return $this->status() >= 400 && $this->status() < 500;
}
/**
* Determine if the response indicates a server error occurred.
* @return bool
*/
public function serverError()
{
return $this->status() >= 500;
}
/**
* Determine if the given offset exists.
*
* @param string $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return array_key_exists( $offset,$this->json() );
}
/**
* Get the value for a given offset.
*
* @param string $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->json()[$offset];
}
/**
* Set the value at the given offset.
*
* @param string $offset
* @param mixed $value
* @return void
*
* @throws \LogicException
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset,$value)
{
throw new LogicException( 'Response data may not be mutated using array access.' );
}
/**
* Unset the value at the given offset.
*
* @param string $offset
* @return void
*
* @throws \LogicException
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
throw new LogicException( 'Response data may not be mutated using array access.' );
}
/**
* Get the body of the response.
*
* @return string
*/
public function __toString()
{
return $this->body();
}
protected function mapWithKeys($items,callable $callback)
{
$result = [];
foreach ( $items as $key => $value ) {
$assoc = $callback( $value,$key );
foreach ( $assoc as $mapKey => $mapValue ) {
$result[$mapKey] = $mapValue;
}
}
return new static( $result );
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace yzh52521\EasyHttp;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\ConnectException;
class Retry
{
public function handle($retries,$sleep)
{
return Middleware::retry($this->decider($retries), $this->delay($sleep));
}
protected function decider(int $times)
{
return function (
$retries,
Request $request,
Response $response = null,
RequestException $exception = null
) use ($times) {
// 超过最大重试次数,不再重试
if ($retries >= $times) {
return false;
}
return $exception instanceof ConnectException || $exception instanceof ServerException || ($response && $response->getStatusCode() >= 500);
};
}
/**
* 返回一个匿名函数,该匿名函数返回下次重试的时间(毫秒)
* @param int $retry_delay
* @return \Closure
*/
protected function delay(int $retry_delay)
{
return function ($retries) use ($retry_delay) {
return $retry_delay * $retries;
};
}
}